diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 809fed12..a817334f 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "4.13.0"
+ ".": "4.14.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ec9a42d6..65797c0e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 4.14.0 (2026-01-06)
+
+Full Changelog: [v4.13.0...v4.14.0](https://github.com/openai/openai-java/compare/v4.13.0...v4.14.0)
+
+### Features
+
+* **client:** add `HttpRequest#url()` method ([cf7d459](https://github.com/openai/openai-java/commit/cf7d459019625c6e08f376efb588c8aff332bf8c))
+
## 4.13.0 (2025-12-19)
Full Changelog: [v4.12.0...v4.13.0](https://github.com/openai/openai-java/compare/v4.12.0...v4.13.0)
diff --git a/LICENSE b/LICENSE
index f011417a..cbb5bb26 100644
--- a/LICENSE
+++ b/LICENSE
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
- Copyright 2025 OpenAI
+ Copyright 2026 OpenAI
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
diff --git a/README.md b/README.md
index 617395b3..71cba80c 100644
--- a/README.md
+++ b/README.md
@@ -2,8 +2,8 @@
-[](https://central.sonatype.com/artifact/com.openai/openai-java/4.13.0)
-[](https://javadoc.io/doc/com.openai/openai-java/4.13.0)
+[](https://central.sonatype.com/artifact/com.openai/openai-java/4.14.0)
+[](https://javadoc.io/doc/com.openai/openai-java/4.14.0)
@@ -11,7 +11,7 @@ The OpenAI Java SDK provides convenient access to the [OpenAI REST API](https://
-The REST API documentation can be found on [platform.openai.com](https://platform.openai.com/docs). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.openai/openai-java/4.13.0).
+The REST API documentation can be found on [platform.openai.com](https://platform.openai.com/docs). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.openai/openai-java/4.14.0).
@@ -24,7 +24,7 @@ The REST API documentation can be found on [platform.openai.com](https://platfor
### Gradle
```kotlin
-implementation("com.openai:openai-java:4.13.0")
+implementation("com.openai:openai-java:4.14.0")
```
### Maven
@@ -33,7 +33,7 @@ implementation("com.openai:openai-java:4.13.0")
com.openai
openai-java
- 4.13.0
+ 4.14.0
```
@@ -1342,7 +1342,7 @@ If you're using Spring Boot, then you can use the SDK's [Spring Boot starter](ht
#### Gradle
```kotlin
-implementation("com.openai:openai-java-spring-boot-starter:4.13.0")
+implementation("com.openai:openai-java-spring-boot-starter:4.14.0")
```
#### Maven
@@ -1351,7 +1351,7 @@ implementation("com.openai:openai-java-spring-boot-starter:4.13.0")
com.openai
openai-java-spring-boot-starter
- 4.13.0
+ 4.14.0
```
diff --git a/build.gradle.kts b/build.gradle.kts
index 5a5edaa3..a4f6abaf 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -8,7 +8,7 @@ repositories {
allprojects {
group = "com.openai"
- version = "4.13.0" // x-release-please-version
+ version = "4.14.0" // x-release-please-version
}
subprojects {
diff --git a/openai-java-core/src/main/kotlin/com/openai/core/http/HttpRequest.kt b/openai-java-core/src/main/kotlin/com/openai/core/http/HttpRequest.kt
index 3e5a659a..7dfa41d1 100644
--- a/openai-java-core/src/main/kotlin/com/openai/core/http/HttpRequest.kt
+++ b/openai-java-core/src/main/kotlin/com/openai/core/http/HttpRequest.kt
@@ -2,6 +2,7 @@ package com.openai.core.http
import com.openai.core.checkRequired
import com.openai.core.toImmutable
+import java.net.URLEncoder
class HttpRequest
private constructor(
@@ -13,6 +14,35 @@ private constructor(
@get:JvmName("body") val body: HttpRequestBody?,
) {
+ fun url(): String = buildString {
+ append(baseUrl)
+
+ pathSegments.forEach { segment ->
+ if (!endsWith("/")) {
+ append("/")
+ }
+ append(URLEncoder.encode(segment, "UTF-8"))
+ }
+
+ if (queryParams.isEmpty()) {
+ return@buildString
+ }
+
+ append("?")
+ var isFirst = true
+ queryParams.keys().forEach { key ->
+ queryParams.values(key).forEach { value ->
+ if (!isFirst) {
+ append("&")
+ }
+ append(URLEncoder.encode(key, "UTF-8"))
+ append("=")
+ append(URLEncoder.encode(value, "UTF-8"))
+ isFirst = false
+ }
+ }
+ }
+
fun toBuilder(): Builder = Builder().from(this)
override fun toString(): String =
diff --git a/openai-java-core/src/test/kotlin/com/openai/core/http/HttpRequestTest.kt b/openai-java-core/src/test/kotlin/com/openai/core/http/HttpRequestTest.kt
new file mode 100644
index 00000000..7a0fa54c
--- /dev/null
+++ b/openai-java-core/src/test/kotlin/com/openai/core/http/HttpRequestTest.kt
@@ -0,0 +1,110 @@
+package com.openai.core.http
+
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.EnumSource
+
+internal class HttpRequestTest {
+
+ enum class UrlTestCase(val request: HttpRequest, val expectedUrl: String) {
+ BASE_URL_ONLY(
+ HttpRequest.builder().method(HttpMethod.GET).baseUrl("https://api.example.com").build(),
+ expectedUrl = "https://api.example.com",
+ ),
+ BASE_URL_WITH_TRAILING_SLASH(
+ HttpRequest.builder()
+ .method(HttpMethod.GET)
+ .baseUrl("https://api.example.com/")
+ .build(),
+ expectedUrl = "https://api.example.com/",
+ ),
+ SINGLE_PATH_SEGMENT(
+ HttpRequest.builder()
+ .method(HttpMethod.GET)
+ .baseUrl("https://api.example.com")
+ .addPathSegment("users")
+ .build(),
+ expectedUrl = "https://api.example.com/users",
+ ),
+ MULTIPLE_PATH_SEGMENTS(
+ HttpRequest.builder()
+ .method(HttpMethod.GET)
+ .baseUrl("https://api.example.com")
+ .addPathSegments("users", "123", "profile")
+ .build(),
+ expectedUrl = "https://api.example.com/users/123/profile",
+ ),
+ PATH_SEGMENT_WITH_SPECIAL_CHARS(
+ HttpRequest.builder()
+ .method(HttpMethod.GET)
+ .baseUrl("https://api.example.com")
+ .addPathSegment("user name")
+ .build(),
+ expectedUrl = "https://api.example.com/user+name",
+ ),
+ SINGLE_QUERY_PARAM(
+ HttpRequest.builder()
+ .method(HttpMethod.GET)
+ .baseUrl("https://api.example.com")
+ .addPathSegment("users")
+ .putQueryParam("limit", "10")
+ .build(),
+ expectedUrl = "https://api.example.com/users?limit=10",
+ ),
+ MULTIPLE_QUERY_PARAMS(
+ HttpRequest.builder()
+ .method(HttpMethod.GET)
+ .baseUrl("https://api.example.com")
+ .addPathSegment("users")
+ .putQueryParam("limit", "10")
+ .putQueryParam("offset", "20")
+ .build(),
+ expectedUrl = "https://api.example.com/users?limit=10&offset=20",
+ ),
+ QUERY_PARAM_WITH_SPECIAL_CHARS(
+ HttpRequest.builder()
+ .method(HttpMethod.GET)
+ .baseUrl("https://api.example.com")
+ .addPathSegment("search")
+ .putQueryParam("q", "hello world")
+ .build(),
+ expectedUrl = "https://api.example.com/search?q=hello+world",
+ ),
+ MULTIPLE_VALUES_SAME_PARAM(
+ HttpRequest.builder()
+ .method(HttpMethod.GET)
+ .baseUrl("https://api.example.com")
+ .addPathSegment("users")
+ .putQueryParams("tags", listOf("admin", "user"))
+ .build(),
+ expectedUrl = "https://api.example.com/users?tags=admin&tags=user",
+ ),
+ BASE_URL_WITH_TRAILING_SLASH_AND_PATH(
+ HttpRequest.builder()
+ .method(HttpMethod.GET)
+ .baseUrl("https://api.example.com/")
+ .addPathSegment("users")
+ .build(),
+ expectedUrl = "https://api.example.com/users",
+ ),
+ COMPLEX_URL(
+ HttpRequest.builder()
+ .method(HttpMethod.POST)
+ .baseUrl("https://api.example.com")
+ .addPathSegments("v1", "users", "123")
+ .putQueryParams("include", listOf("profile", "settings"))
+ .putQueryParam("format", "json")
+ .build(),
+ expectedUrl =
+ "https://api.example.com/v1/users/123?include=profile&include=settings&format=json",
+ ),
+ }
+
+ @ParameterizedTest
+ @EnumSource
+ fun url(testCase: UrlTestCase) {
+ val actualUrl = testCase.request.url()
+
+ assertThat(actualUrl).isEqualTo(testCase.expectedUrl)
+ }
+}