From 4b0829fe433f37672d944db44a56407bfe48d481 Mon Sep 17 00:00:00 2001 From: dpiet-oai <326640584+dpiet-oai@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:46:46 +0000 Subject: [PATCH] fix(live): avoid contextual transcript casing scans Castiron-Internal-PR: https://github.com/openai/openai-java-internal/pull/281 Castiron-Source-SHA: e8eed1e3442f6a92193179487d796912242cae6d Castiron-Public-Base-SHA: 848ef53c94498e795587d571900202ea383601d4 --- .castiron.stats.yml | 6 +++--- .../com/openai/helpers/LiveTranscriptGrouper.kt | 12 ++++++++---- .../openai/helpers/LiveTranscriptGrouperTest.kt | 17 +++++++++++++++++ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/.castiron.stats.yml b/.castiron.stats.yml index d04f294a7..9efd98756 100644 --- a/.castiron.stats.yml +++ b/.castiron.stats.yml @@ -1,8 +1,8 @@ schema_version: 1 -generation_id: b935ba17-9980-45bd-b8c7-1939d1b03aba +generation_id: 0cdeb809-627b-4caa-9340-887e46d7979f openapi_spec_hash: e81fa0fd2133715dabef9f00a42557a9 openapi_transformed_spec_hash: bea7a30d23deed3fc8f1c6413a38d354 config_hash: dda57991526f80e9f99d8f9621460eaa -codegen_sha: 9306aff2af8ad922f591ebbc1d29362cf9271bc8 +codegen_sha: ca58cce22b36f9f5586a16379bc977d20f193a4a codegen_hash: bda6b5fe61d5a094103b7cbc38597c82109f4c14bb23d4501d6c3e1569f75878 -public_codegen_sha: 43d374297ff620990fa45fdd39a32079c3905e30 +public_codegen_sha: 3a713a4b40be032d22b5aa19b393ee32178ea142 diff --git a/openai-java-core/src/main/kotlin/com/openai/helpers/LiveTranscriptGrouper.kt b/openai-java-core/src/main/kotlin/com/openai/helpers/LiveTranscriptGrouper.kt index b7eb92bf8..b4c3d9b3e 100644 --- a/openai-java-core/src/main/kotlin/com/openai/helpers/LiveTranscriptGrouper.kt +++ b/openai-java-core/src/main/kotlin/com/openai/helpers/LiveTranscriptGrouper.kt @@ -191,9 +191,13 @@ class LiveTranscriptGrouper private constructor(builder: Builder) : AutoCloseabl ) private fun normalize(value: String): String = - whitespace - .replace(value.lowercase(Locale.ROOT).replace('ς', 'σ').replace('-', ' '), " ") - .trim { it == ' ' || it in punctuation } + whitespace.replace(foldCase(value).replace('-', ' '), " ").trim { + it == ' ' || it in punctuation + } + + // Sigma forms are equivalent here; fold before ROOT casing to avoid contextual scans. + private fun foldCase(value: String): String = + value.replace('\u03A3', '\u03C3').lowercase(Locale.ROOT).replace('ς', 'σ') } private class AckNode { @@ -221,7 +225,7 @@ class LiveTranscriptGrouper private constructor(builder: Builder) : AutoCloseabl highSurrogate = text.last().takeIf(Char::isHighSurrogate) val end = text.length - if (highSurrogate == null) 0 else 1 // Folding final sigma keeps ROOT lowercase independent of fragment boundaries. - val lower = text.substring(0, end).lowercase(Locale.ROOT).replace('ς', 'σ') + val lower = foldCase(text.substring(0, end)) for (raw in lower) { val char = if (raw == '-' || whitespace.matches(raw.toString())) ' ' else raw val trim = char == ' ' || char in punctuation diff --git a/openai-java-core/src/test/kotlin/com/openai/helpers/LiveTranscriptGrouperTest.kt b/openai-java-core/src/test/kotlin/com/openai/helpers/LiveTranscriptGrouperTest.kt index 721c11dcc..dccf3ee31 100644 --- a/openai-java-core/src/test/kotlin/com/openai/helpers/LiveTranscriptGrouperTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/helpers/LiveTranscriptGrouperTest.kt @@ -22,6 +22,7 @@ import java.util.concurrent.TimeoutException import java.util.function.Consumer import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Assertions.assertTimeout import org.junit.jupiter.api.Test internal class LiveTranscriptGrouperTest { @@ -336,6 +337,22 @@ internal class LiveTranscriptGrouperTest { assertThat(finalized(events).map { it.segment().text() }).containsExactly("hello") } + @Test + fun longSigmaTranscriptPreservesTextWithoutContextualCasingDelay() { + val text = "\u03A3".repeat(32_000) + val events = updates() + assertTimeout(Duration.ofSeconds(5)) { + LiveTranscriptGrouper.create { events.add(it) } + .use { grouper -> + grouper.push(input("u1", "hello", 0, 100)) + grouper.push(output("a1", text, 0, 100)) + grouper.push(input("u2", " again", 100, 200)) + } + } + assertThat(finalized(events).map { it.segment().text() }) + .containsExactly("hello again", text) + } + @Test fun acknowledgmentCaseMatchingDoesNotDependOnDeltaBoundaries() { // U+0130 expands on ROOT lowercasing; Greek sigma has a final and an ordinary form.