diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index fe87cd917..cc51f6f8e 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "0.43.0"
+ ".": "0.44.0"
}
\ No newline at end of file
diff --git a/.stats.yml b/.stats.yml
index b80d385d6..e0e1a71ed 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 80
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-4bce8217a697c729ac98046d4caf2c9e826b54c427fb0ab4f98e549a2e0ce31c.yml
openapi_spec_hash: 7996d2c34cc44fe2ce9ffe93c0ab774e
-config_hash: 578c5bff4208d560c0c280f13324409f
+config_hash: bcd2cacdcb9fae9938f273cd167f613c
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 920a70bb5..971a9bf33 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,23 @@
# Changelog
+## 0.44.0 (2025-04-04)
+
+Full Changelog: [v0.43.0...v0.44.0](https://github.com/openai/openai-java/compare/v0.43.0...v0.44.0)
+
+### Features
+
+* **api:** manual updates ([331ec66](https://github.com/openai/openai-java/commit/331ec66baf6add46ec79ba56842f31065dffff33))
+
+
+### Bug Fixes
+
+* **client:** translate streaming `IOException` into custom exception ([#397](https://github.com/openai/openai-java/issues/397)) ([bc5c577](https://github.com/openai/openai-java/commit/bc5c57721fd7115800d7a9fd2e38631e695caa25))
+
+
+### Performance Improvements
+
+* **client:** cached parsed type in `HttpResponseFor` ([#395](https://github.com/openai/openai-java/issues/395)) ([259b75a](https://github.com/openai/openai-java/commit/259b75a9e3a3f6de2e123b171bd739facdecd819))
+
## 0.43.0 (2025-04-02)
Full Changelog: [v0.42.0...v0.43.0](https://github.com/openai/openai-java/compare/v0.42.0...v0.43.0)
diff --git a/README.md b/README.md
index ea25d2d2b..f32118d84 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,8 @@
-[](https://central.sonatype.com/artifact/com.openai/openai-java/0.43.0)
-[](https://javadoc.io/doc/com.openai/openai-java/0.43.0)
+[](https://central.sonatype.com/artifact/com.openai/openai-java/0.44.0)
+[](https://javadoc.io/doc/com.openai/openai-java/0.44.0)
@@ -18,7 +18,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 also available on [javadoc.io](https://javadoc.io/doc/com.openai/openai-java/0.43.0).
+The REST API documentation can be found on [platform.openai.com](https://platform.openai.com/docs). Javadocs are also available on [javadoc.io](https://javadoc.io/doc/com.openai/openai-java/0.44.0).
@@ -29,7 +29,7 @@ The REST API documentation can be found on [platform.openai.com](https://platfor
### Gradle
```kotlin
-implementation("com.openai:openai-java:0.43.0")
+implementation("com.openai:openai-java:0.44.0")
```
### Maven
@@ -38,7 +38,7 @@ implementation("com.openai:openai-java:0.43.0")
com.openai
openai-java
- 0.43.0
+ 0.44.0
```
diff --git a/build.gradle.kts b/build.gradle.kts
index 9794d6c54..d7b08ac3c 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -8,7 +8,7 @@ repositories {
allprojects {
group = "com.openai"
- version = "0.43.0" // x-release-please-version
+ version = "0.44.0" // x-release-please-version
}
subprojects {
diff --git a/openai-java-core/src/main/kotlin/com/openai/core/handlers/StreamHandler.kt b/openai-java-core/src/main/kotlin/com/openai/core/handlers/StreamHandler.kt
index e715dadec..13a1949a6 100644
--- a/openai-java-core/src/main/kotlin/com/openai/core/handlers/StreamHandler.kt
+++ b/openai-java-core/src/main/kotlin/com/openai/core/handlers/StreamHandler.kt
@@ -6,6 +6,8 @@ import com.openai.core.http.HttpResponse
import com.openai.core.http.HttpResponse.Handler
import com.openai.core.http.PhantomReachableClosingStreamResponse
import com.openai.core.http.StreamResponse
+import com.openai.errors.OpenAIIoException
+import java.io.IOException
import java.util.stream.Stream
import kotlin.streams.asStream
@@ -14,17 +16,30 @@ internal fun streamHandler(
block: suspend SequenceScope.(response: HttpResponse, lines: Sequence) -> Unit
): Handler> =
object : Handler> {
+
override fun handle(response: HttpResponse): StreamResponse {
val reader = response.body().bufferedReader()
val sequence =
// Wrap in a `CloseableSequence` to avoid performing a read on the `reader`
// after it has been closed, which would throw an `IOException`.
CloseableSequence(
- sequence { reader.useLines { block(response, it) } }.constrainOnce()
+ sequence {
+ reader.useLines { lines ->
+ block(
+ response,
+ // We wrap the `lines` instead of the top-level sequence because
+ // we only want to catch `IOException` from the reader; not from
+ // the user's own code.
+ IOExceptionWrappingSequence(lines),
+ )
+ }
+ }
+ .constrainOnce()
)
return PhantomReachableClosingStreamResponse(
object : StreamResponse {
+
override fun stream(): Stream = sequence.asStream()
override fun close() {
@@ -37,6 +52,30 @@ internal fun streamHandler(
}
}
+/** A sequence that catches, wraps, and rethrows [IOException] as [OpenAIIoException]. */
+private class IOExceptionWrappingSequence(private val sequence: Sequence) : Sequence {
+
+ override fun iterator(): Iterator {
+ val iterator = sequence.iterator()
+ return object : Iterator {
+
+ override fun next(): T =
+ try {
+ iterator.next()
+ } catch (e: IOException) {
+ throw OpenAIIoException("Stream failed", e)
+ }
+
+ override fun hasNext(): Boolean =
+ try {
+ iterator.hasNext()
+ } catch (e: IOException) {
+ throw OpenAIIoException("Stream failed", e)
+ }
+ }
+ }
+}
+
/**
* A sequence that can be closed.
*
@@ -44,11 +83,13 @@ internal fun streamHandler(
* underlying [Iterator.hasNext] method.
*/
private class CloseableSequence(private val sequence: Sequence) : Sequence {
+
private var isClosed: Boolean = false
override fun iterator(): Iterator {
val iterator = sequence.iterator()
return object : Iterator {
+
override fun next(): T = iterator.next()
override fun hasNext(): Boolean = !isClosed && iterator.hasNext()
diff --git a/openai-java-core/src/main/kotlin/com/openai/core/http/HttpResponseFor.kt b/openai-java-core/src/main/kotlin/com/openai/core/http/HttpResponseFor.kt
index eaf211a05..bb1a56199 100644
--- a/openai-java-core/src/main/kotlin/com/openai/core/http/HttpResponseFor.kt
+++ b/openai-java-core/src/main/kotlin/com/openai/core/http/HttpResponseFor.kt
@@ -11,7 +11,9 @@ interface HttpResponseFor : HttpResponse {
internal fun HttpResponse.parseable(parse: () -> T): HttpResponseFor =
object : HttpResponseFor {
- override fun parse(): T = parse()
+ private val parsed: T by lazy { parse() }
+
+ override fun parse(): T = parsed
override fun statusCode(): Int = this@parseable.statusCode()
diff --git a/openai-java-core/src/test/kotlin/com/openai/core/handlers/StreamHandlerTest.kt b/openai-java-core/src/test/kotlin/com/openai/core/handlers/StreamHandlerTest.kt
new file mode 100644
index 000000000..d06392c86
--- /dev/null
+++ b/openai-java-core/src/test/kotlin/com/openai/core/handlers/StreamHandlerTest.kt
@@ -0,0 +1,64 @@
+package com.openai.core.handlers
+
+import com.openai.core.http.Headers
+import com.openai.core.http.HttpResponse
+import com.openai.errors.OpenAIIoException
+import java.io.IOException
+import java.io.InputStream
+import kotlin.test.Test
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.assertThrows
+
+internal class StreamHandlerTest {
+
+ @Test
+ fun streamHandler_whenReaderThrowsIOException_wrapsException() {
+ val handler = streamHandler { _, lines -> lines.forEach {} }
+ val streamResponse = handler.handle(httpResponse("a\nb\nc\n".byteInputStream().throwing()))
+
+ val e = assertThrows { streamResponse.stream().forEach {} }
+ assertThat(e).hasMessage("Stream failed")
+ assertThat(e).hasCauseInstanceOf(IOException::class.java)
+ }
+
+ @Test
+ fun streamHandler_whenBlockThrowsIOException_doesNotWrapException() {
+ val ioException = IOException("BOOM!")
+ val handler =
+ streamHandler { _, lines ->
+ lines.forEachIndexed { index, _ ->
+ if (index == 2) {
+ throw ioException
+ }
+ }
+ }
+ val streamResponse = handler.handle(httpResponse("a\nb\nc\n".byteInputStream()))
+
+ val e = assertThrows { streamResponse.stream().forEach {} }
+ assertThat(e).isSameAs(ioException)
+ }
+
+ private fun httpResponse(body: InputStream): HttpResponse =
+ object : HttpResponse {
+
+ override fun statusCode(): Int = 0
+
+ override fun headers(): Headers = Headers.builder().build()
+
+ override fun body(): InputStream = body
+
+ override fun close() {}
+ }
+
+ private fun InputStream.throwing(): InputStream =
+ object : InputStream() {
+
+ override fun read(): Int {
+ val byte = this@throwing.read()
+ if (byte == -1) {
+ throw IOException("BOOM!")
+ }
+ return byte
+ }
+ }
+}