From 61dc89d15844e3ecd984f938a38e6c17713fb446 Mon Sep 17 00:00:00 2001 From: jewoodev Date: Wed, 27 May 2026 16:12:36 +0900 Subject: [PATCH] Propagate Microsoft Foundry to Azure URL path mode Signed-off-by: jewoodev --- .../ai/openai/setup/OpenAiSetup.java | 18 ++++- .../ai/openai/setup/OpenAiSetupTests.java | 72 +++++++++++++++++++ 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/setup/OpenAiSetup.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/setup/OpenAiSetup.java index 0db1229015..259470cd0a 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/setup/OpenAiSetup.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/setup/OpenAiSetup.java @@ -23,6 +23,7 @@ import java.util.stream.Collectors; import com.openai.azure.AzureOpenAIServiceVersion; +import com.openai.azure.AzureUrlPathMode; import com.openai.azure.credential.AzureApiKeyCredential; import com.openai.client.OpenAIClient; import com.openai.client.OpenAIClientAsync; @@ -71,7 +72,11 @@ public static OpenAIClient setupSyncClient(@Nullable String baseUrl, @Nullable S var modelProvider = detectModelProvider(isAzure, isGitHubModels, baseUrl, azureDeploymentName, azureOpenAiServiceVersion); OpenAIOkHttpClient.Builder builder = OpenAIOkHttpClient.builder(); - builder.baseUrl(calculateBaseUrl(baseUrl, modelProvider, modelName, azureDeploymentName)); + String calculatedBaseUrl = calculateBaseUrl(baseUrl, modelProvider, modelName, azureDeploymentName); + builder.baseUrl(calculatedBaseUrl); + if (modelProvider == ModelProvider.MICROSOFT_FOUNDRY) { + builder.azureUrlPathMode(resolveAzureUrlPathMode(calculatedBaseUrl)); + } String calculatedApiKey = apiKey != null ? apiKey : detectApiKey(modelProvider); if (calculatedApiKey != null) { @@ -129,7 +134,11 @@ public static OpenAIClientAsync setupAsyncClient(@Nullable String baseUrl, @Null var modelProvider = detectModelProvider(isAzure, isGitHubModels, baseUrl, azureDeploymentName, azureOpenAiServiceVersion); OpenAIOkHttpClientAsync.Builder builder = OpenAIOkHttpClientAsync.builder(); - builder.baseUrl(calculateBaseUrl(baseUrl, modelProvider, modelName, azureDeploymentName)); + String calculatedBaseUrl = calculateBaseUrl(baseUrl, modelProvider, modelName, azureDeploymentName); + builder.baseUrl(calculatedBaseUrl); + if (modelProvider == ModelProvider.MICROSOFT_FOUNDRY) { + builder.azureUrlPath(resolveAzureUrlPathMode(calculatedBaseUrl)); + } String calculatedApiKey = apiKey != null ? apiKey : detectApiKey(modelProvider); if (calculatedApiKey != null) { @@ -275,4 +284,9 @@ else if (modelProvider == ModelProvider.GITHUB_MODELS && System.getenv(GITHUB_TO return null; } + static AzureUrlPathMode resolveAzureUrlPathMode(@Nullable String baseUrl) { + return (baseUrl != null && baseUrl.trim().endsWith("/openai/v1")) ? AzureUrlPathMode.UNIFIED + : AzureUrlPathMode.LEGACY; + } + } diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/setup/OpenAiSetupTests.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/setup/OpenAiSetupTests.java index 46898235ed..a653ada052 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/setup/OpenAiSetupTests.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/setup/OpenAiSetupTests.java @@ -21,9 +21,12 @@ import java.util.Collections; import java.util.Map; +import com.openai.azure.AzureUrlPathMode; import com.openai.azure.credential.AzureApiKeyCredential; import com.openai.client.OpenAIClient; +import com.openai.client.OpenAIClientAsync; import com.openai.core.ClientOptions; +import com.openai.credential.Credential; import com.openai.models.ChatModel; import org.junit.jupiter.api.Test; @@ -138,4 +141,73 @@ void setupSyncClient_usesApiKeyHeader_notBearerToken_forMicrosoftFoundry() throw assertThat(options.headers().values("Authorization")).isEmpty(); } + @Test + void setupSyncClient_propagatesLegacyPathMode_forMicrosoftFoundryProxyHost() throws Exception { + OpenAIClient client = OpenAiSetup.setupSyncClient("https://enterprise-proxy.example.com/azure-openai-api", + "my-foundry-key", null, "my-deployment", null, null, true, false, null, Duration.ofSeconds(30), 2, null, + null); + + Field field = client.getClass().getDeclaredField("clientOptions"); + field.setAccessible(true); + ClientOptions options = (ClientOptions) field.get(client); + assertEquals(AzureUrlPathMode.LEGACY, options.azureUrlPathMode()); + } + + @Test + void setupAsyncClient_propagatesLegacyPathMode_forMicrosoftFoundryProxyHost() throws Exception { + OpenAIClientAsync client = OpenAiSetup.setupAsyncClient("https://enterprise-proxy.example.com/azure-openai-api", + "my-foundry-key", null, "my-deployment", null, null, true, false, null, Duration.ofSeconds(30), 2, null, + null); + + Field field = client.getClass().getDeclaredField("clientOptions"); + field.setAccessible(true); + ClientOptions options = (ClientOptions) field.get(client); + assertEquals(AzureUrlPathMode.LEGACY, options.azureUrlPathMode()); + } + + @Test + void setupSyncClient_propagatesUnifiedPathMode_forMicrosoftFoundryUnifiedEndpoint() throws Exception { + OpenAIClient client = OpenAiSetup.setupSyncClient("https://my-resource.openai.azure.com/openai/v1", + "my-foundry-key", null, null, null, null, true, false, null, Duration.ofSeconds(30), 2, null, null); + + Field field = client.getClass().getDeclaredField("clientOptions"); + field.setAccessible(true); + ClientOptions options = (ClientOptions) field.get(client); + assertEquals(AzureUrlPathMode.UNIFIED, options.azureUrlPathMode()); + } + + @Test + void setupSyncClient_keepsAutoPathMode_forNonFoundryProvider() throws Exception { + OpenAIClient client = OpenAiSetup.setupSyncClient(null, "sk-test", null, null, null, null, false, false, null, + Duration.ofSeconds(30), 2, null, null); + + Field field = client.getClass().getDeclaredField("clientOptions"); + field.setAccessible(true); + ClientOptions options = (ClientOptions) field.get(client); + assertEquals(AzureUrlPathMode.AUTO, options.azureUrlPathMode()); + } + + @Test + void setupSyncClient_propagatesLegacyPathMode_whenCredentialProvidedWithoutApiKey() throws Exception { + Credential credential = AzureApiKeyCredential.create("explicit-credential-key"); + OpenAIClient client = OpenAiSetup.setupSyncClient("https://enterprise-proxy.example.com/azure-openai-api", null, + credential, "my-deployment", null, null, true, false, null, Duration.ofSeconds(30), 2, null, null); + + Field field = client.getClass().getDeclaredField("clientOptions"); + field.setAccessible(true); + ClientOptions options = (ClientOptions) field.get(client); + assertEquals(AzureUrlPathMode.LEGACY, options.azureUrlPathMode()); + } + + @Test + void setupSyncClient_keepsAutoPathMode_forGitHubModelsProvider() throws Exception { + OpenAIClient client = OpenAiSetup.setupSyncClient(null, "ghp-test", null, null, null, null, false, true, null, + Duration.ofSeconds(30), 2, null, null); + + Field field = client.getClass().getDeclaredField("clientOptions"); + field.setAccessible(true); + ClientOptions options = (ClientOptions) field.get(client); + assertEquals(AzureUrlPathMode.AUTO, options.azureUrlPathMode()); + } + }