Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.43.0"
".": "0.44.0"
}
2 changes: 1 addition & 1 deletion .stats.yml
Original file line number Diff line number Diff line change
@@ -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
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@

<!-- x-release-please-start-version -->

[![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)

<!-- x-release-please-end -->

The OpenAI Java SDK provides convenient access to the [OpenAI REST API](https://platform.openai.com/docs) from applications written in Java.

<!-- x-release-please-start-version -->

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).

<!-- x-release-please-end -->

Expand All @@ -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
Expand All @@ -38,7 +38,7 @@ implementation("com.openai:openai-java:0.43.0")
<dependency>
<groupId>com.openai</groupId>
<artifactId>openai-java</artifactId>
<version>0.43.0</version>
<version>0.44.0</version>
</dependency>
```

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -14,17 +16,30 @@ internal fun <T> streamHandler(
block: suspend SequenceScope<T>.(response: HttpResponse, lines: Sequence<String>) -> Unit
): Handler<StreamResponse<T>> =
object : Handler<StreamResponse<T>> {

override fun handle(response: HttpResponse): StreamResponse<T> {
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<T> {

override fun stream(): Stream<T> = sequence.asStream()

override fun close() {
Expand All @@ -37,18 +52,44 @@ internal fun <T> streamHandler(
}
}

/** A sequence that catches, wraps, and rethrows [IOException] as [OpenAIIoException]. */
private class IOExceptionWrappingSequence<T>(private val sequence: Sequence<T>) : Sequence<T> {

override fun iterator(): Iterator<T> {
val iterator = sequence.iterator()
return object : Iterator<T> {

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.
*
* Once [close] is called, it will not yield more elements. It will also no longer consult the
* underlying [Iterator.hasNext] method.
*/
private class CloseableSequence<T>(private val sequence: Sequence<T>) : Sequence<T> {

private var isClosed: Boolean = false

override fun iterator(): Iterator<T> {
val iterator = sequence.iterator()
return object : Iterator<T> {

override fun next(): T = iterator.next()

override fun hasNext(): Boolean = !isClosed && iterator.hasNext()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ interface HttpResponseFor<T> : HttpResponse {
internal fun <T> HttpResponse.parseable(parse: () -> T): HttpResponseFor<T> =
object : HttpResponseFor<T> {

override fun parse(): T = parse()
private val parsed: T by lazy { parse() }

override fun parse(): T = parsed

override fun statusCode(): Int = [email protected]()

Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> { _, lines -> lines.forEach {} }
val streamResponse = handler.handle(httpResponse("a\nb\nc\n".byteInputStream().throwing()))

val e = assertThrows<OpenAIIoException> { 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<String> { _, lines ->
lines.forEachIndexed { index, _ ->
if (index == 2) {
throw ioException
}
}
}
val streamResponse = handler.handle(httpResponse("a\nb\nc\n".byteInputStream()))

val e = assertThrows<IOException> { 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 = [email protected]()
if (byte == -1) {
throw IOException("BOOM!")
}
return byte
}
}
}
Loading