Skip to content

Commit b98f533

Browse files
jpalvarezlTomerAberbach
authored andcommitted
feat(client): support new unified Azure URL scheme (#554)
* Drafting proposal to allow for flexible base URL # Conflicts: # openai-java-core/src/main/kotlin/com/openai/azure/HttpRequestBuilderExtensions.kt # openai-java-core/src/main/kotlin/com/openai/core/ClientOptions.kt * Added utility functions for the unified v1 and legacy azure endpoints * Renamed tests * Aligned impl with spec, fixed tests, added docs * Simplified unified endpoint check * PR feedback and sample cleanup * Fixed some final renamings * Update HttpRequestBuilderExtensions.kt comments * Update Azure service version clause in ClientOptions.kt * Added AzureUrlCategory enum to centralize Azure endpoint identification, PR comments * Adjusting toggle and related methods sorting * Adjusted weird comment. * Made new enum and method internal * Update openai-java-core/src/main/kotlin/com/openai/azure/AzureUrlCategory.kt Co-authored-by: Tomer Aberbach <[email protected]> * Changed azureLegacyPaths toggle to AzureUrlPathMode enum * chore: refactor --------- Co-authored-by: Tomer Aberbach <[email protected]> Co-authored-by: Tomer Aberbach <[email protected]>
1 parent d9e9bb9 commit b98f533

File tree

14 files changed

+431
-44
lines changed

14 files changed

+431
-44
lines changed

openai-java-client-okhttp/src/main/kotlin/com/openai/client/okhttp/OpenAIOkHttpClient.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package com.openai.client.okhttp
44

55
import com.fasterxml.jackson.databind.json.JsonMapper
66
import com.openai.azure.AzureOpenAIServiceVersion
7+
import com.openai.azure.AzureUrlPathMode
78
import com.openai.client.OpenAIClient
89
import com.openai.client.OpenAIClientImpl
910
import com.openai.core.ClientOptions
@@ -12,7 +13,6 @@ import com.openai.core.http.AsyncStreamResponse
1213
import com.openai.core.http.Headers
1314
import com.openai.core.http.HttpClient
1415
import com.openai.core.http.QueryParams
15-
import com.openai.core.jsonMapper
1616
import com.openai.credential.Credential
1717
import java.net.Proxy
1818
import java.time.Clock
@@ -204,6 +204,10 @@ class OpenAIOkHttpClient private constructor() {
204204
clientOptions.azureServiceVersion(azureServiceVersion)
205205
}
206206

207+
fun azureUrlPathMode(azureUrlPathMode: AzureUrlPathMode) = apply {
208+
clientOptions.azureUrlPathMode(azureUrlPathMode)
209+
}
210+
207211
fun organization(organization: String?) = apply { clientOptions.organization(organization) }
208212

209213
/** Alias for calling [Builder.organization] with `organization.orElse(null)`. */

openai-java-client-okhttp/src/main/kotlin/com/openai/client/okhttp/OpenAIOkHttpClientAsync.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package com.openai.client.okhttp
44

55
import com.fasterxml.jackson.databind.json.JsonMapper
66
import com.openai.azure.AzureOpenAIServiceVersion
7+
import com.openai.azure.AzureUrlPathMode
78
import com.openai.client.OpenAIClientAsync
89
import com.openai.client.OpenAIClientAsyncImpl
910
import com.openai.core.ClientOptions
@@ -12,7 +13,6 @@ import com.openai.core.http.AsyncStreamResponse
1213
import com.openai.core.http.Headers
1314
import com.openai.core.http.HttpClient
1415
import com.openai.core.http.QueryParams
15-
import com.openai.core.jsonMapper
1616
import com.openai.credential.Credential
1717
import java.net.Proxy
1818
import java.time.Clock
@@ -204,6 +204,10 @@ class OpenAIOkHttpClientAsync private constructor() {
204204
clientOptions.azureServiceVersion(azureServiceVersion)
205205
}
206206

207+
fun azureUrlPath(azureUrlPathMode: AzureUrlPathMode) = apply {
208+
clientOptions.azureUrlPathMode(azureUrlPathMode)
209+
}
210+
207211
fun organization(organization: String?) = apply { clientOptions.organization(organization) }
208212

209213
/** Alias for calling [Builder.organization] with `organization.orElse(null)`. */
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package com.openai.azure
2+
3+
import java.net.URI
4+
5+
/** Represents the category of an Azure URL. */
6+
internal enum class AzureUrlCategory {
7+
/** Azure host _not_ ending with `/openai/v1`. */
8+
AZURE_LEGACY,
9+
/** Azure host ending with `/openai/v1`. */
10+
AZURE_UNIFIED,
11+
/** Anything else. */
12+
NON_AZURE;
13+
14+
fun isAzure(): Boolean =
15+
when (this) {
16+
AZURE_LEGACY,
17+
AZURE_UNIFIED -> true
18+
NON_AZURE -> false
19+
}
20+
21+
companion object {
22+
23+
fun categorizeBaseUrl(baseUrl: String, pathMode: AzureUrlPathMode): AzureUrlCategory {
24+
val trimmedBaseUrl = baseUrl.trim().trimEnd('/')
25+
val host = URI.create(trimmedBaseUrl).host
26+
return when {
27+
// Azure OpenAI resource URL with the old schema.
28+
host.endsWith(".openai.azure.com", ignoreCase = true) ||
29+
// Azure OpenAI resource URL with the OpenAI unified schema.
30+
host.endsWith(".services.ai.azure.com", ignoreCase = true) ||
31+
// Azure OpenAI resource URL, but with a schema different to the known ones.
32+
host.endsWith(".azure-api.net", ignoreCase = true) ||
33+
host.endsWith(".cognitiveservices.azure.com", ignoreCase = true) ->
34+
when (pathMode) {
35+
AzureUrlPathMode.LEGACY -> AZURE_LEGACY
36+
AzureUrlPathMode.UNIFIED ->
37+
if (trimmedBaseUrl.endsWith("/openai/v1")) AZURE_UNIFIED
38+
else AZURE_LEGACY
39+
}
40+
41+
else -> NON_AZURE
42+
}
43+
}
44+
}
45+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package com.openai.azure
2+
3+
/**
4+
* To force the deployment or model named to be part of the URL path for Azure OpenAI requests, use
5+
* [AzureUrlPathMode.LEGACY]. The default is [AzureUrlPathMode.UNIFIED].
6+
*/
7+
enum class AzureUrlPathMode {
8+
LEGACY,
9+
UNIFIED,
10+
}

openai-java-core/src/main/kotlin/com/openai/azure/HttpRequestBuilderExtensions.kt

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,17 @@ package com.openai.azure
22

33
import com.openai.core.ClientOptions
44
import com.openai.core.http.HttpRequest
5-
import com.openai.core.isAzureEndpoint
65
import com.openai.credential.BearerTokenCredential
76

87
@JvmSynthetic
98
internal fun HttpRequest.Builder.addPathSegmentsForAzure(
109
clientOptions: ClientOptions,
1110
deploymentModel: String?,
1211
): HttpRequest.Builder = apply {
13-
if (isAzureEndpoint(clientOptions.baseUrl())) {
12+
val urlCategory =
13+
AzureUrlCategory.categorizeBaseUrl(clientOptions.baseUrl(), clientOptions.azureUrlPathMode)
14+
if (urlCategory == AzureUrlCategory.AZURE_LEGACY) {
15+
// Legacy known Azure endpoints are treated the old way.
1416
addPathSegment("openai")
1517
deploymentModel?.let { addPathSegments("deployments", it) }
1618
}
@@ -20,10 +22,9 @@ internal fun HttpRequest.Builder.addPathSegmentsForAzure(
2022
internal fun HttpRequest.Builder.replaceBearerTokenForAzure(
2123
clientOptions: ClientOptions
2224
): HttpRequest.Builder = apply {
23-
if (
24-
isAzureEndpoint(clientOptions.baseUrl()) &&
25-
clientOptions.credential is BearerTokenCredential
26-
) {
25+
val urlCategory =
26+
AzureUrlCategory.categorizeBaseUrl(clientOptions.baseUrl(), clientOptions.azureUrlPathMode)
27+
if (urlCategory.isAzure() && clientOptions.credential is BearerTokenCredential) {
2728
replaceHeaders("Authorization", "Bearer ${clientOptions.credential.token()}")
2829
}
2930
}

openai-java-core/src/main/kotlin/com/openai/core/ClientOptions.kt

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ package com.openai.core
44

55
import com.fasterxml.jackson.databind.json.JsonMapper
66
import com.openai.azure.AzureOpenAIServiceVersion
7+
import com.openai.azure.AzureUrlCategory
8+
import com.openai.azure.AzureUrlPathMode
79
import com.openai.azure.credential.AzureApiKeyCredential
810
import com.openai.core.http.AsyncStreamResponse
911
import com.openai.core.http.Headers
@@ -98,6 +100,7 @@ private constructor(
98100
@get:JvmName("maxRetries") val maxRetries: Int,
99101
@get:JvmName("credential") val credential: Credential,
100102
@get:JvmName("azureServiceVersion") val azureServiceVersion: AzureOpenAIServiceVersion?,
103+
@get:JvmName("azureUrlPathMode") val azureUrlPathMode: AzureUrlPathMode,
101104
private val organization: String?,
102105
private val project: String?,
103106
private val webhookSecret: String?,
@@ -163,6 +166,7 @@ private constructor(
163166
private var maxRetries: Int = 2
164167
private var credential: Credential? = null
165168
private var azureServiceVersion: AzureOpenAIServiceVersion? = null
169+
private var azureUrlPathMode: AzureUrlPathMode = AzureUrlPathMode.UNIFIED
166170
private var organization: String? = null
167171
private var project: String? = null
168172
private var webhookSecret: String? = null
@@ -182,6 +186,7 @@ private constructor(
182186
maxRetries = clientOptions.maxRetries
183187
credential = clientOptions.credential
184188
azureServiceVersion = clientOptions.azureServiceVersion
189+
azureUrlPathMode = clientOptions.azureUrlPathMode
185190
organization = clientOptions.organization
186191
project = clientOptions.project
187192
webhookSecret = clientOptions.webhookSecret
@@ -297,6 +302,10 @@ private constructor(
297302
this.azureServiceVersion = azureServiceVersion
298303
}
299304

305+
fun azureUrlPathMode(azureUrlPathMode: AzureUrlPathMode) = apply {
306+
this.azureUrlPathMode = azureUrlPathMode
307+
}
308+
300309
fun organization(organization: String?) = apply { this.organization = organization }
301310

302311
/** Alias for calling [Builder.organization] with `organization.orElse(null)`. */
@@ -485,14 +494,20 @@ private constructor(
485494
}
486495

487496
baseUrl?.let {
488-
if (isAzureEndpoint(it)) {
489-
// Default Azure OpenAI version is used if Azure user doesn't
490-
// specific a service API version in 'queryParams'.
491-
replaceQueryParams(
492-
"api-version",
493-
(azureServiceVersion ?: AzureOpenAIServiceVersion.latestStableVersion())
494-
.value,
495-
)
497+
when (AzureUrlCategory.categorizeBaseUrl(it, azureUrlPathMode)) {
498+
// Legacy Azure routes will still require an api-version value.
499+
AzureUrlCategory.AZURE_LEGACY ->
500+
replaceQueryParams(
501+
"api-version",
502+
(azureServiceVersion ?: AzureOpenAIServiceVersion.latestStableVersion())
503+
.value,
504+
)
505+
// We only add the value if it's defined by the user for unified Azure routes.
506+
AzureUrlCategory.AZURE_UNIFIED ->
507+
azureServiceVersion?.let { version ->
508+
replaceQueryParams("api-version", version.value)
509+
}
510+
AzureUrlCategory.NON_AZURE -> {}
496511
}
497512
}
498513

@@ -532,6 +547,7 @@ private constructor(
532547
maxRetries,
533548
credential,
534549
azureServiceVersion,
550+
azureUrlPathMode,
535551
organization,
536552
project,
537553
webhookSecret,

openai-java-core/src/main/kotlin/com/openai/core/Utils.kt

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,6 @@ internal fun Any?.contentToString(): String {
9191
return string
9292
}
9393

94-
@JvmSynthetic
95-
internal fun isAzureEndpoint(baseUrl: String): Boolean {
96-
// Azure Endpoint should be in the format of `https://<region>.openai.azure.com`.
97-
// Or `https://<region>.azure-api.net` for Azure OpenAI Management URL.
98-
// Or `<user>-random-<region>.cognitiveservices.azure.com`.
99-
val trimmedBaseUrl = baseUrl.trim().trimEnd('/')
100-
return trimmedBaseUrl.endsWith(".openai.azure.com", true) ||
101-
trimmedBaseUrl.endsWith(".azure-api.net", true) ||
102-
trimmedBaseUrl.endsWith(".cognitiveservices.azure.com", true)
103-
}
104-
10594
internal interface Enum
10695

10796
/**

0 commit comments

Comments
 (0)