Skip to content

Commit a9782b0

Browse files
release: 0.32.0 (#278)
* feat(client): accept `InputStream` and `Path` for file params (#277) * feat(client): accept `InputStream` and `Path` for file params * docs: document file uploads in readme (#279) * release: 0.32.0 --------- Co-authored-by: stainless-app[bot] <142633134+stainless-app[bot]@users.noreply.github.com> Co-authored-by: Stainless Bot <[email protected]>
1 parent c9a19c3 commit a9782b0

21 files changed

+520
-105
lines changed

.release-please-manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
{
2-
".": "0.31.1"
2+
".": "0.32.0"
33
}

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## 0.32.0 (2025-03-06)
4+
5+
Full Changelog: [v0.31.1...v0.32.0](https://github.com/openai/openai-java/compare/v0.31.1...v0.32.0)
6+
7+
### Features
8+
9+
* **client:** accept `InputStream` and `Path` for file params ([3fda365](https://github.com/openai/openai-java/commit/3fda365a9d1b228ec4799a57137a288c25441dc7))
10+
* **client:** accept `InputStream` and `Path` for file params ([#277](https://github.com/openai/openai-java/issues/277)) ([04f2d3c](https://github.com/openai/openai-java/commit/04f2d3c1042187fe2b52ec132ff3a0529f5b1316))
11+
12+
13+
### Documentation
14+
15+
* document file uploads in readme ([#279](https://github.com/openai/openai-java/issues/279)) ([96e4969](https://github.com/openai/openai-java/commit/96e4969a322da35f6ac13324f69d9170c7280aff))
16+
317
## 0.31.1 (2025-03-05)
418

519
Full Changelog: [v0.31.0...v0.31.1](https://github.com/openai/openai-java/compare/v0.31.0...v0.31.1)

README.md

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
1010
<!-- x-release-please-start-version -->
1111

12-
[![Maven Central](https://img.shields.io/maven-central/v/com.openai/openai-java)](https://central.sonatype.com/artifact/com.openai/openai-java/0.31.1)
13-
[![javadoc](https://javadoc.io/badge2/com.openai/openai-java/0.31.1/javadoc.svg)](https://javadoc.io/doc/com.openai/openai-java/0.31.1)
12+
[![Maven Central](https://img.shields.io/maven-central/v/com.openai/openai-java)](https://central.sonatype.com/artifact/com.openai/openai-java/0.32.0)
13+
[![javadoc](https://javadoc.io/badge2/com.openai/openai-java/0.32.0/javadoc.svg)](https://javadoc.io/doc/com.openai/openai-java/0.32.0)
1414

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

@@ -25,7 +25,7 @@ The REST API documentation can be found on [platform.openai.com](https://platfor
2525
### Gradle
2626

2727
```kotlin
28-
implementation("com.openai:openai-java:0.31.1")
28+
implementation("com.openai:openai-java:0.32.0")
2929
```
3030

3131
### Maven
@@ -34,7 +34,7 @@ implementation("com.openai:openai-java:0.31.1")
3434
<dependency>
3535
<groupId>com.openai</groupId>
3636
<artifactId>openai-java</artifactId>
37-
<version>0.31.1</version>
37+
<version>0.32.0</version>
3838
</dependency>
3939
```
4040

@@ -263,6 +263,74 @@ OpenAIClient client = OpenAIOkHttpClient.builder()
263263
.build();
264264
```
265265

266+
## File uploads
267+
268+
The SDK defines methods that accept files.
269+
270+
To upload a file, pass a [`Path`](https://docs.oracle.com/javase/8/docs/api/java/nio/file/Path.html):
271+
272+
```java
273+
import com.openai.models.FileCreateParams;
274+
import com.openai.models.FileObject;
275+
import com.openai.models.FilePurpose;
276+
import java.nio.file.Paths;
277+
278+
FileCreateParams params = FileCreateParams.builder()
279+
.purpose(FilePurpose.FINE_TUNE)
280+
.file(Paths.get("input.jsonl"))
281+
.build();
282+
FileObject fileObject = client.files().create(params);
283+
```
284+
285+
Or an arbitrary [`InputStream`](https://docs.oracle.com/javase/8/docs/api/java/io/InputStream.html):
286+
287+
```java
288+
import com.openai.models.FileCreateParams;
289+
import com.openai.models.FileObject;
290+
import com.openai.models.FilePurpose;
291+
import java.net.URL;
292+
293+
FileCreateParams params = FileCreateParams.builder()
294+
.purpose(FilePurpose.FINE_TUNE)
295+
.file(new URL("https://example.com").openStream())
296+
.build();
297+
FileObject fileObject = client.files().create(params);
298+
```
299+
300+
Or a `byte[]` array:
301+
302+
```java
303+
import com.openai.models.FileCreateParams;
304+
import com.openai.models.FileObject;
305+
import com.openai.models.FilePurpose;
306+
307+
FileCreateParams params = FileCreateParams.builder()
308+
.purpose(FilePurpose.FINE_TUNE)
309+
.file("content".getBytes())
310+
.build();
311+
FileObject fileObject = client.files().create(params);
312+
```
313+
314+
Note that when passing a non-`Path` its filename is unknown so it will not be included in the request. To manually set a filename, pass a `MultipartField`:
315+
316+
```java
317+
import com.openai.core.MultipartField;
318+
import com.openai.models.FileCreateParams;
319+
import com.openai.models.FileObject;
320+
import com.openai.models.FilePurpose;
321+
import java.io.InputStream;
322+
import java.net.URL;
323+
324+
FileCreateParams params = FileCreateParams.builder()
325+
.purpose(FilePurpose.FINE_TUNE)
326+
.file(MultipartField.<InputStream>builder()
327+
.value(new URL("https://example.com").openStream())
328+
.filename("input.jsonl")
329+
.build())
330+
.build();
331+
FileObject fileObject = client.files().create(params);
332+
```
333+
266334
## Binary responses
267335

268336
The SDK defines methods that return binary responses, which are used for API responses that shouldn't necessarily be parsed, like non-JSON data.

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ repositories {
88

99
allprojects {
1010
group = "com.openai"
11-
version = "0.31.1" // x-release-please-version
11+
version = "0.32.0" // x-release-please-version
1212
}
1313

1414
subprojects {

openai-java-core/src/main/kotlin/com/openai/core/ObjectMappers.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,43 @@
33
package com.openai.core
44

55
import com.fasterxml.jackson.annotation.JsonInclude
6+
import com.fasterxml.jackson.core.JsonGenerator
67
import com.fasterxml.jackson.databind.DeserializationFeature
78
import com.fasterxml.jackson.databind.SerializationFeature
9+
import com.fasterxml.jackson.databind.SerializerProvider
810
import com.fasterxml.jackson.databind.cfg.CoercionAction.Fail
911
import com.fasterxml.jackson.databind.cfg.CoercionInputShape.Integer
1012
import com.fasterxml.jackson.databind.json.JsonMapper
13+
import com.fasterxml.jackson.databind.module.SimpleModule
1114
import com.fasterxml.jackson.datatype.jdk8.Jdk8Module
1215
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
1316
import com.fasterxml.jackson.module.kotlin.jacksonMapperBuilder
17+
import java.io.InputStream
1418

1519
fun jsonMapper(): JsonMapper =
1620
jacksonMapperBuilder()
1721
.addModule(Jdk8Module())
1822
.addModule(JavaTimeModule())
23+
.addModule(SimpleModule().addSerializer(InputStreamJsonSerializer))
1924
.serializationInclusion(JsonInclude.Include.NON_ABSENT)
2025
.disable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE)
2126
.disable(SerializationFeature.FLUSH_AFTER_WRITE_VALUE)
2227
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
2328
.disable(SerializationFeature.WRITE_DURATIONS_AS_TIMESTAMPS)
2429
.withCoercionConfig(String::class.java) { it.setCoercion(Integer, Fail) }
2530
.build()
31+
32+
private object InputStreamJsonSerializer : BaseSerializer<InputStream>(InputStream::class) {
33+
34+
override fun serialize(
35+
value: InputStream?,
36+
gen: JsonGenerator?,
37+
serializers: SerializerProvider?,
38+
) {
39+
if (value == null) {
40+
gen?.writeNull()
41+
} else {
42+
value.use { gen?.writeBinary(it.readBytes()) }
43+
}
44+
}
45+
}

openai-java-core/src/main/kotlin/com/openai/core/Values.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import com.fasterxml.jackson.databind.node.JsonNodeType.POJO
2727
import com.fasterxml.jackson.databind.node.JsonNodeType.STRING
2828
import com.fasterxml.jackson.databind.ser.std.NullSerializer
2929
import com.openai.errors.OpenAIInvalidDataException
30+
import java.io.InputStream
3031
import java.util.Objects
3132
import java.util.Optional
3233

@@ -508,7 +509,10 @@ private constructor(
508509
return MultipartField(
509510
value,
510511
contentType
511-
?: if (value is KnownValue && value.value is ByteArray)
512+
?: if (
513+
value is KnownValue &&
514+
(value.value is InputStream || value.value is ByteArray)
515+
)
512516
"application/octet-stream"
513517
else "text/plain; charset=utf-8",
514518
filename,

openai-java-core/src/main/kotlin/com/openai/core/http/HttpRequestBodies.kt

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import com.fasterxml.jackson.databind.json.JsonMapper
99
import com.fasterxml.jackson.databind.node.JsonNodeType
1010
import com.openai.core.MultipartField
1111
import com.openai.errors.OpenAIInvalidDataException
12+
import java.io.ByteArrayInputStream
13+
import java.io.InputStream
1214
import java.io.OutputStream
1315
import kotlin.jvm.optionals.getOrNull
1416
import org.apache.hc.client5.http.entity.mime.MultipartEntityBuilder
@@ -41,8 +43,18 @@ internal fun multipartFormData(
4143
MultipartEntityBuilder.create()
4244
.apply {
4345
fields.forEach { (name, field) ->
44-
val node = jsonMapper.valueToTree<JsonNode>(field.value)
45-
serializePart(name, node).forEach { (name, bytes) ->
46+
val knownValue = field.value.asKnown().getOrNull()
47+
val parts =
48+
if (knownValue is InputStream) {
49+
// Read directly from the `InputStream` instead of reading it all
50+
// into memory due to the `jsonMapper` serialization below.
51+
sequenceOf(name to knownValue)
52+
} else {
53+
val node = jsonMapper.valueToTree<JsonNode>(field.value)
54+
serializePart(name, node)
55+
}
56+
57+
parts.forEach { (name, bytes) ->
4658
addBinaryBody(
4759
name,
4860
bytes,
@@ -55,16 +67,19 @@ internal fun multipartFormData(
5567
.build()
5668
}
5769

58-
private fun serializePart(name: String, node: JsonNode): Sequence<Pair<String, ByteArray>> =
70+
private fun serializePart(
71+
name: String,
72+
node: JsonNode,
73+
): Sequence<Pair<String, InputStream>> =
5974
when (node.nodeType) {
6075
JsonNodeType.MISSING,
6176
JsonNodeType.NULL -> emptySequence()
62-
JsonNodeType.BINARY -> sequenceOf(name to node.binaryValue())
63-
JsonNodeType.STRING -> sequenceOf(name to node.textValue().toByteArray())
77+
JsonNodeType.BINARY -> sequenceOf(name to ByteArrayInputStream(node.binaryValue()))
78+
JsonNodeType.STRING -> sequenceOf(name to node.textValue().toInputStream())
6479
JsonNodeType.BOOLEAN ->
65-
sequenceOf(name to node.booleanValue().toString().toByteArray())
80+
sequenceOf(name to node.booleanValue().toString().toInputStream())
6681
JsonNodeType.NUMBER ->
67-
sequenceOf(name to node.numberValue().toString().toByteArray())
82+
sequenceOf(name to node.numberValue().toString().toInputStream())
6883
JsonNodeType.ARRAY ->
6984
node.elements().asSequence().flatMap { element ->
7085
serializePart("$name[]", element)
@@ -78,6 +93,8 @@ internal fun multipartFormData(
7893
throw OpenAIInvalidDataException("Unexpected JsonNode type: ${node.nodeType}")
7994
}
8095

96+
private fun String.toInputStream(): InputStream = ByteArrayInputStream(toByteArray())
97+
8198
override fun writeTo(outputStream: OutputStream) = entity.writeTo(outputStream)
8299

83100
override fun contentType(): String = entity.contentType

openai-java-core/src/main/kotlin/com/openai/models/AudioTranscriptionCreateParams.kt

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,13 @@ import com.openai.core.http.Headers
1414
import com.openai.core.http.QueryParams
1515
import com.openai.core.toImmutable
1616
import com.openai.errors.OpenAIInvalidDataException
17+
import java.io.ByteArrayInputStream
18+
import java.io.InputStream
19+
import java.nio.file.Path
1720
import java.util.Objects
1821
import java.util.Optional
22+
import kotlin.io.path.inputStream
23+
import kotlin.io.path.name
1924

2025
/** Transcribes audio into the input language. */
2126
class AudioTranscriptionCreateParams
@@ -29,7 +34,7 @@ private constructor(
2934
* The audio file object (not file name) to transcribe, in one of these formats: flac, mp3, mp4,
3035
* mpeg, mpga, m4a, ogg, wav, or webm.
3136
*/
32-
fun file(): ByteArray = body.file()
37+
fun file(): InputStream = body.file()
3338

3439
/**
3540
* ID of the model to use. Only `whisper-1` (which is powered by our open source Whisper V2
@@ -78,7 +83,7 @@ private constructor(
7883
* The audio file object (not file name) to transcribe, in one of these formats: flac, mp3, mp4,
7984
* mpeg, mpga, m4a, ogg, wav, or webm.
8085
*/
81-
fun _file(): MultipartField<ByteArray> = body._file()
86+
fun _file(): MultipartField<InputStream> = body._file()
8287

8388
/**
8489
* ID of the model to use. Only `whisper-1` (which is powered by our open source Whisper V2
@@ -148,7 +153,7 @@ private constructor(
148153
class Body
149154
@JsonCreator
150155
private constructor(
151-
private val file: MultipartField<ByteArray>,
156+
private val file: MultipartField<InputStream>,
152157
private val model: MultipartField<AudioModel>,
153158
private val language: MultipartField<String>,
154159
private val prompt: MultipartField<String>,
@@ -161,7 +166,7 @@ private constructor(
161166
* The audio file object (not file name) to transcribe, in one of these formats: flac, mp3,
162167
* mp4, mpeg, mpga, m4a, ogg, wav, or webm.
163168
*/
164-
fun file(): ByteArray = file.value.getRequired("file")
169+
fun file(): InputStream = file.value.getRequired("file")
165170

166171
/**
167172
* ID of the model to use. Only `whisper-1` (which is powered by our open source Whisper V2
@@ -214,7 +219,7 @@ private constructor(
214219
* The audio file object (not file name) to transcribe, in one of these formats: flac, mp3,
215220
* mp4, mpeg, mpga, m4a, ogg, wav, or webm.
216221
*/
217-
fun _file(): MultipartField<ByteArray> = file
222+
fun _file(): MultipartField<InputStream> = file
218223

219224
/**
220225
* ID of the model to use. Only `whisper-1` (which is powered by our open source Whisper V2
@@ -296,7 +301,7 @@ private constructor(
296301
/** A builder for [Body]. */
297302
class Builder internal constructor() {
298303

299-
private var file: MultipartField<ByteArray>? = null
304+
private var file: MultipartField<InputStream>? = null
300305
private var model: MultipartField<AudioModel>? = null
301306
private var language: MultipartField<String> = MultipartField.of(null)
302307
private var prompt: MultipartField<String> = MultipartField.of(null)
@@ -321,13 +326,31 @@ private constructor(
321326
* The audio file object (not file name) to transcribe, in one of these formats: flac,
322327
* mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm.
323328
*/
324-
fun file(file: ByteArray) = file(MultipartField.of(file))
329+
fun file(file: InputStream) = file(MultipartField.of(file))
325330

326331
/**
327332
* The audio file object (not file name) to transcribe, in one of these formats: flac,
328333
* mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm.
329334
*/
330-
fun file(file: MultipartField<ByteArray>) = apply { this.file = file }
335+
fun file(file: MultipartField<InputStream>) = apply { this.file = file }
336+
337+
/**
338+
* The audio file object (not file name) to transcribe, in one of these formats: flac,
339+
* mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm.
340+
*/
341+
fun file(file: ByteArray) = file(ByteArrayInputStream(file))
342+
343+
/**
344+
* The audio file object (not file name) to transcribe, in one of these formats: flac,
345+
* mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm.
346+
*/
347+
fun file(file: Path) =
348+
file(
349+
MultipartField.builder<InputStream>()
350+
.value(file.inputStream())
351+
.filename(file.name)
352+
.build()
353+
)
331354

332355
/**
333356
* ID of the model to use. Only `whisper-1` (which is powered by our open source Whisper
@@ -506,6 +529,18 @@ private constructor(
506529
additionalQueryParams = audioTranscriptionCreateParams.additionalQueryParams.toBuilder()
507530
}
508531

532+
/**
533+
* The audio file object (not file name) to transcribe, in one of these formats: flac, mp3,
534+
* mp4, mpeg, mpga, m4a, ogg, wav, or webm.
535+
*/
536+
fun file(file: InputStream) = apply { body.file(file) }
537+
538+
/**
539+
* The audio file object (not file name) to transcribe, in one of these formats: flac, mp3,
540+
* mp4, mpeg, mpga, m4a, ogg, wav, or webm.
541+
*/
542+
fun file(file: MultipartField<InputStream>) = apply { body.file(file) }
543+
509544
/**
510545
* The audio file object (not file name) to transcribe, in one of these formats: flac, mp3,
511546
* mp4, mpeg, mpga, m4a, ogg, wav, or webm.
@@ -516,7 +551,7 @@ private constructor(
516551
* The audio file object (not file name) to transcribe, in one of these formats: flac, mp3,
517552
* mp4, mpeg, mpga, m4a, ogg, wav, or webm.
518553
*/
519-
fun file(file: MultipartField<ByteArray>) = apply { body.file(file) }
554+
fun file(file: Path) = apply { body.file(file) }
520555

521556
/**
522557
* ID of the model to use. Only `whisper-1` (which is powered by our open source Whisper V2

0 commit comments

Comments
 (0)