diff --git a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiAudioTranscriptionModel.java b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiAudioTranscriptionModel.java index cb919363c40..5ad1369f73e 100644 --- a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiAudioTranscriptionModel.java +++ b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiAudioTranscriptionModel.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 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. @@ -23,6 +23,7 @@ import com.azure.ai.openai.models.AudioTranscriptionFormat; import com.azure.ai.openai.models.AudioTranscriptionOptions; import com.azure.ai.openai.models.AudioTranscriptionTimestampGranularity; +import com.azure.core.http.rest.RequestOptions; import com.azure.core.http.rest.Response; import org.springframework.ai.audio.transcription.AudioTranscription; @@ -83,9 +84,18 @@ public AudioTranscriptionResponse call(AudioTranscriptionPrompt audioTranscripti AudioTranscriptionOptions audioTranscriptionOptions = toAudioTranscriptionOptions(audioTranscriptionPrompt); AudioTranscriptionFormat responseFormat = audioTranscriptionOptions.getResponseFormat(); + RequestOptions requestOptions = new RequestOptions(); + + if (audioTranscriptionPrompt + .getOptions() instanceof AzureOpenAiAudioTranscriptionOptions azureOpenAiAudioTranscriptionOptions + && null != azureOpenAiAudioTranscriptionOptions.getApiVersion()) { + requestOptions.addQueryParam("api-version", azureOpenAiAudioTranscriptionOptions.getApiVersion()); + } if (JSON_FORMATS.contains(responseFormat)) { - var audioTranscription = this.openAIClient.getAudioTranscription(deploymentOrModelName, FILENAME_MARKER, - audioTranscriptionOptions); + var audioTranscription = this.openAIClient + .getAudioTranscriptionWithResponse(deploymentOrModelName, FILENAME_MARKER, audioTranscriptionOptions, + requestOptions) + .getValue(); List words = null; if (audioTranscription.getWords() != null) { @@ -120,7 +130,7 @@ public AudioTranscriptionResponse call(AudioTranscriptionPrompt audioTranscripti } else { Response audioTranscription = this.openAIClient.getAudioTranscriptionTextWithResponse( - deploymentOrModelName, FILENAME_MARKER, audioTranscriptionOptions, null); + deploymentOrModelName, FILENAME_MARKER, audioTranscriptionOptions, requestOptions); String text = audioTranscription.getValue(); AudioTranscription transcript = new AudioTranscription(text); return new AudioTranscriptionResponse(transcript, AzureOpenAiAudioTranscriptionResponseMetadata.from(text)); diff --git a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiAudioTranscriptionOptions.java b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiAudioTranscriptionOptions.java index e6ce7592bf8..df807ae2d35 100644 --- a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiAudioTranscriptionOptions.java +++ b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiAudioTranscriptionOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 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. @@ -20,6 +20,7 @@ import com.azure.ai.openai.models.AudioTranscriptionFormat; import com.azure.ai.openai.models.AudioTranscriptionTimestampGranularity; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; import com.fasterxml.jackson.annotation.JsonProperty; @@ -67,6 +68,12 @@ public class AzureOpenAiAudioTranscriptionOptions implements AudioTranscriptionO private @JsonProperty("timestamp_granularities") List granularityType; + /** + * The explicit Azure AI Foundry Models API version to use for this request. latest if + * not otherwise specified. + */ + @JsonIgnore private String apiVersion; + public static Builder builder() { return new Builder(); } @@ -128,6 +135,14 @@ public void setGranularityType(List granularityType) { this.granularityType = granularityType; } + public String getApiVersion() { + return apiVersion; + } + + public void setApiVersion(String apiVersion) { + this.apiVersion = apiVersion; + } + @Override public int hashCode() { final int prime = 31; @@ -136,6 +151,7 @@ public int hashCode() { result = prime * result + ((this.prompt == null) ? 0 : this.prompt.hashCode()); result = prime * result + ((this.language == null) ? 0 : this.language.hashCode()); result = prime * result + ((this.responseFormat == null) ? 0 : this.responseFormat.hashCode()); + result = prime * result + ((this.apiVersion == null) ? 0 : this.apiVersion.hashCode()); return result; } @@ -175,6 +191,16 @@ else if (!this.prompt.equals(other.prompt)) { else if (!this.language.equals(other.language)) { return false; } + + if (this.apiVersion == null) { + if (other.apiVersion != null) { + return false; + } + } + else if (!this.apiVersion.equals(other.apiVersion)) { + return false; + } + if (this.responseFormat == null) { return other.responseFormat == null; } @@ -302,6 +328,11 @@ public Builder granularityType(List granularityType) { return this; } + public Builder apiVersion(String apiVersion) { + this.options.apiVersion = apiVersion; + return this; + } + public AzureOpenAiAudioTranscriptionOptions build() { Assert.hasText(this.options.model, "model must not be empty"); Assert.notNull(this.options.responseFormat, "response_format must not be null"); diff --git a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java index 3f659671c4d..98d4b42904d 100644 --- a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java +++ b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java @@ -56,6 +56,7 @@ import com.azure.ai.openai.models.ContentFilterResultsForPrompt; import com.azure.ai.openai.models.FunctionCall; import com.azure.ai.openai.models.ReasoningEffortValue; +import com.azure.core.http.rest.RequestOptions; import com.azure.core.util.BinaryData; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; @@ -265,7 +266,16 @@ public ChatResponse internalCall(Prompt prompt, ChatResponse previousChatRespons ChatCompletionsOptions options = toAzureChatCompletionsOptions(prompt); ChatCompletionsOptionsAccessHelper.setStream(options, false); - ChatCompletions chatCompletions = this.openAIClient.getChatCompletions(options.getModel(), options); + RequestOptions requestOptions = new RequestOptions(); + if (prompt.getOptions() instanceof AzureOpenAiChatOptions azureOpenAiChatOptions + && null != azureOpenAiChatOptions.getApiVersion()) { + requestOptions.addQueryParam("api-version", azureOpenAiChatOptions.getApiVersion()); + } + + ChatCompletions chatCompletions = this.openAIClient + .getChatCompletionsWithResponse(options.getModel(), BinaryData.fromObject(options), requestOptions) + .getValue() + .toObject(ChatCompletions.class); ChatResponse chatResponse = toChatResponse(chatCompletions, previousChatResponse); observationContext.setResponse(chatResponse); return chatResponse; diff --git a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatOptions.java b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatOptions.java index da442b4ad4d..3d0458c5921 100644 --- a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatOptions.java +++ b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 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. @@ -216,6 +216,13 @@ public class AzureOpenAiChatOptions implements ToolCallingChatOptions { @JsonProperty("reasoning_effort") private String reasoningEffort; + /** + * The explicit Azure AI Foundry Models API version to use for this request. latest if + * not otherwise specified. + */ + @JsonIgnore + private String apiVersion; + @Override @JsonIgnore public List getToolCallbacks() { @@ -288,6 +295,7 @@ public static AzureOpenAiChatOptions fromOptions(AzureOpenAiChatOptions fromOpti .toolCallbacks( fromOptions.getToolCallbacks() != null ? new ArrayList<>(fromOptions.getToolCallbacks()) : null) .toolNames(fromOptions.getToolNames() != null ? new HashSet<>(fromOptions.getToolNames()) : null) + .apiVersion(fromOptions.getApiVersion()) .build(); } @@ -482,6 +490,14 @@ public void setStreamOptions(ChatCompletionStreamOptions streamOptions) { this.streamOptions = streamOptions; } + public String getApiVersion() { + return apiVersion; + } + + public void setApiVersion(String apiVersion) { + this.apiVersion = apiVersion; + } + @Override @SuppressWarnings("unchecked") public AzureOpenAiChatOptions copy() { @@ -512,7 +528,8 @@ public boolean equals(Object o) { && Objects.equals(this.toolContext, that.toolContext) && Objects.equals(this.maxTokens, that.maxTokens) && Objects.equals(this.frequencyPenalty, that.frequencyPenalty) && Objects.equals(this.presencePenalty, that.presencePenalty) - && Objects.equals(this.temperature, that.temperature) && Objects.equals(this.topP, that.topP); + && Objects.equals(this.temperature, that.temperature) && Objects.equals(this.topP, that.topP) + && Objects.equals(this.apiVersion, that.apiVersion); } @Override @@ -521,7 +538,7 @@ public int hashCode() { this.toolCallbacks, this.toolNames, this.internalToolExecutionEnabled, this.seed, this.logprobs, this.topLogProbs, this.enhancements, this.streamOptions, this.reasoningEffort, this.enableStreamUsage, this.toolContext, this.maxTokens, this.frequencyPenalty, this.presencePenalty, this.temperature, - this.topP); + this.topP, this.apiVersion); } public static class Builder { @@ -664,6 +681,11 @@ public Builder internalToolExecutionEnabled(@Nullable Boolean internalToolExecut return this; } + public Builder apiVersion(String apiVersion) { + this.options.setApiVersion(apiVersion); + return this; + } + public AzureOpenAiChatOptions build() { return this.options; } diff --git a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiEmbeddingModel.java b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiEmbeddingModel.java index c63ed598ba8..f7d9c41797a 100644 --- a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiEmbeddingModel.java +++ b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiEmbeddingModel.java @@ -24,6 +24,8 @@ import com.azure.ai.openai.models.Embeddings; import com.azure.ai.openai.models.EmbeddingsOptions; import com.azure.ai.openai.models.EmbeddingsUsage; +import com.azure.core.http.rest.RequestOptions; +import com.azure.core.util.BinaryData; import io.micrometer.observation.ObservationRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -140,7 +142,18 @@ public EmbeddingResponse call(EmbeddingRequest embeddingRequest) { .observation(this.observationConvention, DEFAULT_OBSERVATION_CONVENTION, () -> observationContext, this.observationRegistry) .observe(() -> { - Embeddings embeddings = this.azureOpenAiClient.getEmbeddings(azureOptions.getModel(), azureOptions); + + RequestOptions requestOptions = new RequestOptions(); + + if (null != options.getApiVersion()) { + requestOptions.addQueryParam("api-version", options.getApiVersion()); + } + + Embeddings embeddings = this.azureOpenAiClient + .getEmbeddingsWithResponse(azureOptions.getModel(), BinaryData.fromObject(azureOptions), + requestOptions) + .getValue() + .toObject(Embeddings.class); logger.debug("Embeddings retrieved"); var embeddingResponse = generateEmbeddingResponse(embeddings); diff --git a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiEmbeddingOptions.java b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiEmbeddingOptions.java index 52431f13bb2..3e19159f9a5 100644 --- a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiEmbeddingOptions.java +++ b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiEmbeddingOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 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. @@ -57,6 +57,12 @@ public class AzureOpenAiEmbeddingOptions implements EmbeddingOptions { */ private Integer dimensions; + /** + * The explicit Azure AI Foundry Models API version to use for this request. latest if + * not otherwise specified. + */ + private String apiVersion; + public static Builder builder() { return new Builder(); } @@ -105,6 +111,14 @@ public void setDimensions(Integer dimensions) { this.dimensions = dimensions; } + public String getApiVersion() { + return apiVersion; + } + + public void setApiVersion(String apiVersion) { + this.apiVersion = apiVersion; + } + public com.azure.ai.openai.models.EmbeddingsOptions toAzureOptions(List instructions) { var azureOptions = new com.azure.ai.openai.models.EmbeddingsOptions(instructions); @@ -144,6 +158,9 @@ public Builder merge(EmbeddingOptions from) { if (castFrom.getDimensions() != null) { this.options.setDimensions(castFrom.getDimensions()); } + if (castFrom.getApiVersion() != null) { + this.options.setApiVersion(castFrom.getApiVersion()); + } } return this; } @@ -177,6 +194,11 @@ public Builder dimensions(Integer dimensions) { return this; } + public Builder apiVersion(String apiVersion) { + this.options.apiVersion = apiVersion; + return this; + } + public AzureOpenAiEmbeddingOptions build() { return this.options; } diff --git a/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureChatCompletionsOptionsTests.java b/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureChatCompletionsOptionsTests.java index 2c13ced5636..2a66cde8f8f 100644 --- a/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureChatCompletionsOptionsTests.java +++ b/models/spring-ai-azure-openai/src/test/java/org/springframework/ai/azure/openai/AzureChatCompletionsOptionsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2023-2025 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. @@ -70,6 +70,7 @@ public void createRequestWithChatOptions() { .topLogprobs(5) .enhancements(mockAzureChatEnhancementConfiguration) .responseFormat(AzureOpenAiResponseFormat.builder().type(Type.TEXT).build()) + .apiVersion("preview") .build(); var client = AzureOpenAiChatModel.builder() @@ -79,6 +80,7 @@ public void createRequestWithChatOptions() { var requestOptions = client.toAzureChatCompletionsOptions(new Prompt("Test message content")); + assertThat(client.getDefaultOptions().getApiVersion()).isEqualTo("preview"); assertThat(requestOptions.getMessages()).hasSize(1); assertThat(requestOptions.getModel()).isEqualTo("DEFAULT_MODEL");