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 @@ -[![Maven Central](https://img.shields.io/maven-central/v/com.openai/openai-java)](https://central.sonatype.com/artifact/com.openai/openai-java/0.43.0) -[![javadoc](https://javadoc.io/badge2/com.openai/openai-java/0.43.0/javadoc.svg)](https://javadoc.io/doc/com.openai/openai-java/0.43.0) +[![Maven Central](https://img.shields.io/maven-central/v/com.openai/openai-java)](https://central.sonatype.com/artifact/com.openai/openai-java/0.44.0) +[![javadoc](https://javadoc.io/badge2/com.openai/openai-java/0.44.0/javadoc.svg)](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 + } + } +}