From d7cd8d9c34e66328f11d481131eab8a9141299ba Mon Sep 17 00:00:00 2001 From: Lucas Christian Date: Sat, 18 Jul 2026 23:02:12 -0300 Subject: [PATCH 1/4] fix: turn diarized duration into optional Signed-off-by: Lucas Christian --- .../transcriptions/TranscriptionDiarized.kt | 25 ++++++++++------ .../TranscriptionDiarizedTest.kt | 30 +++++++++++++++++-- 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarized.kt b/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarized.kt index f8a368ef3..4b3ad8395 100644 --- a/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarized.kt +++ b/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarized.kt @@ -58,10 +58,10 @@ private constructor( /** * Duration of the input audio in seconds. * - * @throws OpenAIInvalidDataException if the JSON field has an unexpected type or is - * unexpectedly missing or null (e.g. if the server responded with an unexpected value). + * @throws OpenAIInvalidDataException if the JSON field has an unexpected type (e.g. if the + * server responded with an unexpected value). */ - fun duration(): Double = duration.getRequired("duration") + fun duration(): Optional = duration.getOptional("duration") /** * Segments of the transcript annotated with timestamps and speaker labels. @@ -149,7 +149,6 @@ private constructor( * * The following fields are required: * ```java - * .duration() * .segments() * .text() * ``` @@ -160,7 +159,7 @@ private constructor( /** A builder for [TranscriptionDiarized]. */ class Builder internal constructor() { - private var duration: JsonField? = null + private var duration: JsonField = JsonMissing.of() private var segments: JsonField>? = null private var task: JsonValue = JsonValue.from("transcribe") private var text: JsonField? = null @@ -178,7 +177,17 @@ private constructor( } /** Duration of the input audio in seconds. */ - fun duration(duration: Double) = duration(JsonField.of(duration)) + fun duration(duration: Double?) = duration(JsonField.ofNullable(duration)) + + /** + * Alias for [Builder.duration]. + * + * This unboxed primitive overload exists for backwards compatibility. + */ + fun duration(duration: Double) = duration(duration as Double?) + + /** Alias for calling [Builder.duration] with `duration.orElse(null)`. */ + fun duration(duration: Optional) = duration(duration.getOrNull()) /** * Sets [Builder.duration] to an arbitrary JSON value. @@ -294,7 +303,6 @@ private constructor( * * The following fields are required: * ```java - * .duration() * .segments() * .text() * ``` @@ -303,7 +311,7 @@ private constructor( */ fun build(): TranscriptionDiarized = TranscriptionDiarized( - checkRequired("duration", duration), + duration, checkRequired("segments", segments).map { it.toImmutable() }, task, checkRequired("text", text), @@ -327,7 +335,6 @@ private constructor( return@apply } - duration() segments().forEach { it.validate() } _task().let { if (it != JsonValue.from("transcribe")) { diff --git a/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarizedTest.kt b/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarizedTest.kt index 0d0dfd851..60e3e1994 100644 --- a/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarizedTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarizedTest.kt @@ -13,7 +13,6 @@ internal class TranscriptionDiarizedTest { fun create() { val transcriptionDiarized = TranscriptionDiarized.builder() - .duration(0.0) .addSegment( TranscriptionDiarizedSegment.builder() .id("id") @@ -39,7 +38,7 @@ internal class TranscriptionDiarizedTest { ) .build() - assertThat(transcriptionDiarized.duration()).isEqualTo(0.0) + assertThat(transcriptionDiarized.duration()).isEmpty assertThat(transcriptionDiarized.segments()) .containsExactly( TranscriptionDiarizedSegment.builder() @@ -69,6 +68,32 @@ internal class TranscriptionDiarizedTest { ) } + @Test + fun deserializeWithoutDuration() { + val transcriptionDiarized = + jsonMapper() + .readValue( + """ + { + "text": "text", + "segments": [ + { + "id": "id", + "end": 1.0, + "speaker": "speaker", + "start": 0.0, + "text": "text" + } + ] + } + """ + .trimIndent(), + jacksonTypeRef(), + ) + + assertThat(transcriptionDiarized.duration()).isEmpty + } + @Test fun roundtrip() { val jsonMapper = jsonMapper() @@ -107,5 +132,6 @@ internal class TranscriptionDiarizedTest { ) assertThat(roundtrippedTranscriptionDiarized).isEqualTo(transcriptionDiarized) + assertThat(roundtrippedTranscriptionDiarized.duration()).contains(0.0) } } From b1703369d81803088a6d80e4fae744cce3836b8b Mon Sep 17 00:00:00 2001 From: Lucas Christian Date: Sat, 18 Jul 2026 23:10:44 -0300 Subject: [PATCH 2/4] fix: allow missing task in diarized transcriptions Signed-off-by: Lucas Christian --- .../transcriptions/TranscriptionDiarized.kt | 2 +- .../TranscriptionDiarizedTest.kt | 25 ++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarized.kt b/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarized.kt index 4b3ad8395..079bbd162 100644 --- a/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarized.kt +++ b/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarized.kt @@ -337,7 +337,7 @@ private constructor( segments().forEach { it.validate() } _task().let { - if (it != JsonValue.from("transcribe")) { + if (it != JsonMissing.of() && it != JsonValue.from("transcribe")) { throw OpenAIInvalidDataException("'task' is invalid, received $it") } } diff --git a/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarizedTest.kt b/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarizedTest.kt index 60e3e1994..17cb877f8 100644 --- a/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarizedTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarizedTest.kt @@ -3,6 +3,7 @@ package com.openai.models.audio.transcriptions import com.fasterxml.jackson.module.kotlin.jacksonTypeRef +import com.openai.core.JsonValue import com.openai.core.jsonMapper import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test @@ -69,7 +70,7 @@ internal class TranscriptionDiarizedTest { } @Test - fun deserializeWithoutDuration() { + fun deserializeWithoutOptionalFields() { val transcriptionDiarized = jsonMapper() .readValue( @@ -78,6 +79,7 @@ internal class TranscriptionDiarizedTest { "text": "text", "segments": [ { + "type": "transcript.text.segment", "id": "id", "end": 1.0, "speaker": "speaker", @@ -92,6 +94,27 @@ internal class TranscriptionDiarizedTest { ) assertThat(transcriptionDiarized.duration()).isEmpty + assertThat(transcriptionDiarized.isValid()).isTrue() + } + + @Test + fun rejectsUnexpectedTask() { + val transcriptionDiarized = + TranscriptionDiarized.builder() + .addSegment( + TranscriptionDiarizedSegment.builder() + .id("id") + .end(1.0) + .speaker("speaker") + .start(0.0) + .text("text") + .build() + ) + .task(JsonValue.from("translate")) + .text("text") + .build() + + assertThat(transcriptionDiarized.isValid()).isFalse() } @Test From 7c8d0d2db55f95394328ee84278523cd384c8fdf Mon Sep 17 00:00:00 2001 From: Lucas Christian Date: Sat, 18 Jul 2026 23:33:47 -0300 Subject: [PATCH 3/4] fix: validate optional diarized duration Signed-off-by: Lucas Christian --- .../transcriptions/TranscriptionDiarized.kt | 2 ++ .../TranscriptionCreateResponseTest.kt | 18 ++++++++++++ .../TranscriptionDiarizedTest.kt | 28 +++++++++++++++++++ 3 files changed, 48 insertions(+) diff --git a/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarized.kt b/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarized.kt index 079bbd162..9482a0d9b 100644 --- a/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarized.kt +++ b/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarized.kt @@ -335,6 +335,7 @@ private constructor( return@apply } + duration() segments().forEach { it.validate() } _task().let { if (it != JsonMissing.of() && it != JsonValue.from("transcribe")) { @@ -362,6 +363,7 @@ private constructor( @JvmSynthetic internal fun validity(): Int = (if (duration.asKnown().isPresent) 1 else 0) + + (if (segments.asKnown().isPresent) 1 else 0) + (segments.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) + task.let { if (it == JsonValue.from("transcribe")) 1 else 0 } + (if (text.asKnown().isPresent) 1 else 0) + diff --git a/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionCreateResponseTest.kt b/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionCreateResponseTest.kt index c83af3003..1193fbf2a 100644 --- a/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionCreateResponseTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionCreateResponseTest.kt @@ -162,6 +162,24 @@ internal class TranscriptionCreateResponseTest { assertThat(roundtrippedTranscriptionCreateResponse).isEqualTo(transcriptionCreateResponse) } + @Test + fun deserializeDiarizedWithEmptySegments() { + val transcriptionCreateResponse = + jsonMapper() + .readValue( + """ + { + "text": "", + "segments": [] + } + """ + .trimIndent(), + jacksonTypeRef(), + ) + + assertThat(transcriptionCreateResponse.isDiarized()).isTrue() + } + @Test fun ofVerbose() { val verbose = diff --git a/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarizedTest.kt b/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarizedTest.kt index 17cb877f8..931e5039e 100644 --- a/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarizedTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionDiarizedTest.kt @@ -97,6 +97,34 @@ internal class TranscriptionDiarizedTest { assertThat(transcriptionDiarized.isValid()).isTrue() } + @Test + fun rejectsInvalidDuration() { + val transcriptionDiarized = + jsonMapper() + .readValue( + """ + { + "duration": "invalid", + "text": "text", + "segments": [ + { + "type": "transcript.text.segment", + "id": "id", + "end": 1.0, + "speaker": "speaker", + "start": 0.0, + "text": "text" + } + ] + } + """ + .trimIndent(), + jacksonTypeRef(), + ) + + assertThat(transcriptionDiarized.isValid()).isFalse() + } + @Test fun rejectsUnexpectedTask() { val transcriptionDiarized = From 83a861f0b6b5689c4a8df141430ca8b7935101d4 Mon Sep 17 00:00:00 2001 From: Lucas Christian Date: Sat, 18 Jul 2026 23:58:27 -0300 Subject: [PATCH 4/4] fix: preserve transcription response variants with empty segments Signed-off-by: Lucas Christian --- .../transcriptions/TranscriptionVerbose.kt | 1 + .../TranscriptionCreateResponseTest.kt | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionVerbose.kt b/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionVerbose.kt index 46980eaf9..18224dcf9 100644 --- a/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionVerbose.kt +++ b/openai-java-core/src/main/kotlin/com/openai/models/audio/transcriptions/TranscriptionVerbose.kt @@ -370,6 +370,7 @@ private constructor( (if (duration.asKnown().isPresent) 1 else 0) + (if (language.asKnown().isPresent) 1 else 0) + (if (text.asKnown().isPresent) 1 else 0) + + (if (segments.asKnown().isPresent) 1 else 0) + (segments.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) + (usage.asKnown().getOrNull()?.validity() ?: 0) + (words.asKnown().getOrNull()?.sumOf { it.validity().toInt() } ?: 0) diff --git a/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionCreateResponseTest.kt b/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionCreateResponseTest.kt index 1193fbf2a..a7abf6d02 100644 --- a/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionCreateResponseTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/models/audio/transcriptions/TranscriptionCreateResponseTest.kt @@ -180,6 +180,26 @@ internal class TranscriptionCreateResponseTest { assertThat(transcriptionCreateResponse.isDiarized()).isTrue() } + @Test + fun deserializeVerboseWithEmptySegments() { + val transcriptionCreateResponse = + jsonMapper() + .readValue( + """ + { + "duration": 0.0, + "language": "en", + "text": "", + "segments": [] + } + """ + .trimIndent(), + jacksonTypeRef(), + ) + + assertThat(transcriptionCreateResponse.isVerbose()).isTrue() + } + @Test fun ofVerbose() { val verbose =