From 4700b842b887d5db19c46f00e4d96ff7de8f4942 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Fri, 21 Aug 2026 11:50:45 +0100 Subject: [PATCH 1/5] feat(core): expose request body content stream --- .../com/openai/core/http/HttpRequestBody.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/openai-java-core/src/main/kotlin/com/openai/core/http/HttpRequestBody.kt b/openai-java-core/src/main/kotlin/com/openai/core/http/HttpRequestBody.kt index 55bc68a30..b466e67d9 100644 --- a/openai-java-core/src/main/kotlin/com/openai/core/http/HttpRequestBody.kt +++ b/openai-java-core/src/main/kotlin/com/openai/core/http/HttpRequestBody.kt @@ -1,5 +1,7 @@ package com.openai.core.http +import java.io.ByteArrayOutputStream +import java.io.InputStream import java.io.OutputStream import java.lang.AutoCloseable @@ -7,6 +9,19 @@ interface HttpRequestBody : AutoCloseable { fun writeTo(outputStream: OutputStream) + /** + * Returns the request body content as an input stream. + * + * The default implementation buffers the bytes produced by [writeTo] so existing third-party + * implementations remain compatible. Implementations backed by an existing byte array or stream + * should override this method to avoid buffering. + */ + fun content(): InputStream { + val outputStream = ByteArrayOutputStream() + writeTo(outputStream) + return outputStream.toByteArray().inputStream() + } + fun contentType(): String? fun contentLength(): Long From bb070b71f0569287d1b7b4c133b3168d654168eb Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Fri, 21 Aug 2026 11:51:10 +0100 Subject: [PATCH 2/5] feat(core): stream built-in request body content --- .../com/openai/core/http/HttpRequestBodies.kt | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/openai-java-core/src/main/kotlin/com/openai/core/http/HttpRequestBodies.kt b/openai-java-core/src/main/kotlin/com/openai/core/http/HttpRequestBodies.kt index 2c672bb8e..86a2c76b6 100644 --- a/openai-java-core/src/main/kotlin/com/openai/core/http/HttpRequestBodies.kt +++ b/openai-java-core/src/main/kotlin/com/openai/core/http/HttpRequestBodies.kt @@ -11,6 +11,8 @@ import com.openai.errors.OpenAIInvalidDataException import java.io.ByteArrayInputStream import java.io.InputStream import java.io.OutputStream +import java.io.SequenceInputStream +import java.util.Collections import java.util.UUID import kotlin.jvm.optionals.getOrNull @@ -21,6 +23,8 @@ internal inline fun json(jsonMapper: JsonMapper, value: T): HttpRequ override fun writeTo(outputStream: OutputStream) = outputStream.write(bytes) + override fun content(): InputStream = bytes.inputStream() + override fun contentType(): String = "application/json" override fun contentLength(): Long = bytes.size.toLong() @@ -60,6 +64,8 @@ internal fun multipartFormData( outputStream.write(byteArray) } + override fun content(): InputStream = byteArray.inputStream() + override fun contentType(): String = field.contentType override fun contentLength(): Long = byteArray.size.toLong() @@ -75,6 +81,8 @@ internal fun multipartFormData( bytes.copyTo(outputStream) } + override fun content(): InputStream = bytes + override fun contentType(): String = field.contentType override fun contentLength(): Long = -1L @@ -147,6 +155,36 @@ private constructor(private val boundary: String, private val parts: List) outputStream.write(CRLF) } + // This must remain in sync with `writeTo`. + override fun content(): InputStream { + val streams = mutableListOf() + + parts.forEach { part -> + streams.add(DASHDASH.inputStream()) + streams.add(boundaryBytes.inputStream()) + streams.add(CRLF.inputStream()) + + streams.add(CONTENT_DISPOSITION.inputStream()) + streams.add(part.contentDisposition.toByteArray().inputStream()) + streams.add(CRLF.inputStream()) + + streams.add(CONTENT_TYPE.inputStream()) + streams.add(part.contentType.toByteArray().inputStream()) + streams.add(CRLF.inputStream()) + + streams.add(CRLF.inputStream()) + streams.add(part.body.content()) + streams.add(CRLF.inputStream()) + } + + streams.add(DASHDASH.inputStream()) + streams.add(boundaryBytes.inputStream()) + streams.add(DASHDASH.inputStream()) + streams.add(CRLF.inputStream()) + + return SequenceInputStream(Collections.enumeration(streams)) + } + override fun contentType(): String = contentType // This must remain in sync with `writeTo`. From b830b4ae61da8ebb7bf6fc97f1bbd7d406e852c1 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Fri, 21 Aug 2026 11:52:17 +0100 Subject: [PATCH 3/5] test(core): cover request body content streams --- .../core/http/HttpRequestBodyContentTest.kt | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 openai-java-core/src/test/kotlin/com/openai/core/http/HttpRequestBodyContentTest.kt diff --git a/openai-java-core/src/test/kotlin/com/openai/core/http/HttpRequestBodyContentTest.kt b/openai-java-core/src/test/kotlin/com/openai/core/http/HttpRequestBodyContentTest.kt new file mode 100644 index 000000000..38ee82c68 --- /dev/null +++ b/openai-java-core/src/test/kotlin/com/openai/core/http/HttpRequestBodyContentTest.kt @@ -0,0 +1,78 @@ +package com.openai.core.http + +import com.openai.core.MultipartField +import com.openai.core.jsonMapper +import java.io.ByteArrayOutputStream +import java.io.OutputStream +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + +internal class HttpRequestBodyContentTest { + + @Test + fun content_defaultsToWriteTo() { + val body = + object : HttpRequestBody { + override fun writeTo(outputStream: OutputStream) { + outputStream.write("body".toByteArray()) + } + + override fun contentType(): String = "text/plain" + + override fun contentLength(): Long = 4L + + override fun repeatable(): Boolean = true + + override fun close() {} + } + + body.content().use { content -> assertThat(content.readBytes()).isEqualTo("body".toByteArray()) } + } + + @Test + fun multipartContent_matchesWriteTo() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "field" to + MultipartField.builder() + .value("value") + .contentType("text/plain") + .build(), + "binary" to + MultipartField.builder() + .value("abc".toByteArray()) + .contentType("application/octet-stream") + .build(), + ), + ) + + val output = ByteArrayOutputStream() + body.writeTo(output) + + body.content().use { content -> assertThat(content.readBytes()).isEqualTo(output.toByteArray()) } + } + + @Test + fun multipartContent_streamsInputStreamParts() { + val body = + multipartFormData( + jsonMapper(), + mapOf( + "data" to + MultipartField.builder() + .value("stream content".byteInputStream().buffered()) + .contentType("application/octet-stream") + .build() + ), + ) + + val content = body.content().use { it.readBytes().toString(Charsets.UTF_8) } + + assertThat(body.repeatable()).isFalse() + assertThat(content).contains("Content-Disposition: form-data; name=\"data\"") + assertThat(content).contains("Content-Type: application/octet-stream") + assertThat(content).contains("stream content") + } +} From fbe81a013586510705c26f1119c59e8b3508ef87 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Fri, 21 Aug 2026 11:52:51 +0100 Subject: [PATCH 4/5] test(core): verify Java request body compatibility --- .../core/http/HttpRequestBodyJavaTest.java | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 openai-java-core/src/test/java/com/openai/core/http/HttpRequestBodyJavaTest.java diff --git a/openai-java-core/src/test/java/com/openai/core/http/HttpRequestBodyJavaTest.java b/openai-java-core/src/test/java/com/openai/core/http/HttpRequestBodyJavaTest.java new file mode 100644 index 000000000..538104d06 --- /dev/null +++ b/openai-java-core/src/test/java/com/openai/core/http/HttpRequestBodyJavaTest.java @@ -0,0 +1,51 @@ +package com.openai.core.http; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +final class HttpRequestBodyJavaTest { + + @Test + void contentIsJavaDefaultMethod() throws Exception { + assertThat(HttpRequestBody.class.getMethod("content").isDefault()).isTrue(); + + HttpRequestBody body = + new HttpRequestBody() { + @Override + public void writeTo(OutputStream outputStream) { + try { + outputStream.write("body".getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public String contentType() { + return "text/plain"; + } + + @Override + public long contentLength() { + return 4L; + } + + @Override + public boolean repeatable() { + return true; + } + + @Override + public void close() {} + }; + + try (InputStream content = body.content()) { + assertThat(content.readAllBytes()).isEqualTo("body".getBytes(StandardCharsets.UTF_8)); + } + } +} From 3d392208bb9f44d24201bac4e18c2ed18391fd83 Mon Sep 17 00:00:00 2001 From: Sylvester Kaczmarek <16242628+sylvesterkaczmarek@users.noreply.github.com> Date: Fri, 21 Aug 2026 11:53:10 +0100 Subject: [PATCH 5/5] test(core): keep Java compatibility test on Java 8 --- .../java/com/openai/core/http/HttpRequestBodyJavaTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openai-java-core/src/test/java/com/openai/core/http/HttpRequestBodyJavaTest.java b/openai-java-core/src/test/java/com/openai/core/http/HttpRequestBodyJavaTest.java index 538104d06..5c35183b0 100644 --- a/openai-java-core/src/test/java/com/openai/core/http/HttpRequestBodyJavaTest.java +++ b/openai-java-core/src/test/java/com/openai/core/http/HttpRequestBodyJavaTest.java @@ -45,7 +45,10 @@ public void close() {} }; try (InputStream content = body.content()) { - assertThat(content.readAllBytes()).isEqualTo("body".getBytes(StandardCharsets.UTF_8)); + byte[] bytes = new byte[4]; + assertThat(content.read(bytes)).isEqualTo(bytes.length); + assertThat(bytes).isEqualTo("body".getBytes(StandardCharsets.UTF_8)); + assertThat(content.read()).isEqualTo(-1); } } }