Skip to content

Commit 11bae65

Browse files
committed
fix: buffer completion text with linear accumulation
Castiron-Internal-PR: openai/openai-java-internal#99 Castiron-Source-SHA: 5f7f4c7ae92197dc18b6c03d32ee58b218844535 Castiron-Public-Base-SHA: 5ac7585
1 parent 5ac7585 commit 11bae65

3 files changed

Lines changed: 60 additions & 13 deletions

File tree

‎.castiron.stats.yml‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
schema_version: 1
2-
generation_id: 41062568-96ed-43ad-acf7-4e906cec816a
3-
openapi_spec_hash: c7a2088dc0fb8d92f9a934ba4dca2d80
4-
openapi_transformed_spec_hash: 7281a983306f72fc39720d7057f11ef7
5-
config_hash: 60435bd668a5724c530fe7e9d6869e9a
6-
codegen_sha: d8cde0e2bacc733acbb03dc3a3a0d1529f04d9c7
7-
codegen_hash: 6d6c043d25782a4d8090ccdfcc621f32341ba1116513d40b535b89085b9704bd
8-
public_codegen_sha: ff3aa2b162a124130056f2f79c2846068f315842
2+
generation_id: 7667f385-fe72-41e4-bbad-28974f953856
3+
openapi_spec_hash: 3bbe70558d1cb860c1efbfd901285796
4+
openapi_transformed_spec_hash: 2daccabc873f61eec5ef07ab28b20a1a
5+
config_hash: c234968a5221983d6e4315cda9d36d23
6+
codegen_sha: b704e89df9b355295f0a210ded78f0abb7a851ac
7+
codegen_hash: 376441665ba8246e8237451b8fd77dd3d047a2db02eeba4b3373041d6e173bcf
8+
public_codegen_sha: c95741280f105f8280e004ce564afdf944adaff7

‎openai-java-core/src/main/kotlin/com/openai/helpers/ChatCompletionAccumulator.kt‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,13 @@ class ChatCompletionAccumulator private constructor() {
4848
* The accumulated content for each message. The keys correspond to the indexes in
4949
* [messageBuilders].
5050
*/
51-
private val messageContents = mutableMapOf<Long, String>()
51+
private val messageContents = mutableMapOf<Long, StringBuilder>()
5252

5353
/**
5454
* The accumulated refusal for each message. The keys correspond to the indexes in
5555
* [messageBuilders].
5656
*/
57-
private val messageRefusals = mutableMapOf<Long, String>()
57+
private val messageRefusals = mutableMapOf<Long, StringBuilder>()
5858

5959
/**
6060
* The builders for the [ChatCompletion.Choice.Logprobs] of each choice. These are only
@@ -262,8 +262,8 @@ class ChatCompletionAccumulator private constructor() {
262262
internal fun accumulateMessage(index: Long, delta: ChatCompletionChunk.Choice.Delta) {
263263
val messageBuilder = messageBuilders.getOrPut(index) { ChatCompletionMessage.builder() }
264264

265-
delta.content().ifPresent { messageContents[index] = (messageContents[index] ?: "") + it }
266-
delta.refusal().ifPresent { messageRefusals[index] = (messageRefusals[index] ?: "") + it }
265+
delta.content().ifPresent { messageContents.getOrPut(index) { StringBuilder() }.append(it) }
266+
delta.refusal().ifPresent { messageRefusals.getOrPut(index) { StringBuilder() }.append(it) }
267267
// The `role` defaults to "assistant", so if no other `role` is set on the delta, there is
268268
// no need to set it explicitly to anything else.
269269
delta.role().ifPresent { messageBuilder.role(JsonValue.from(it.asString())) }
@@ -325,8 +325,8 @@ class ChatCompletionAccumulator private constructor() {
325325
.getOrElse(index) {
326326
throw OpenAIInvalidDataException("Missing message for index $index.")
327327
}
328-
.content(messageContents[index])
329-
.refusal(messageRefusals[index])
328+
.content(messageContents[index]?.toString())
329+
.refusal(messageRefusals[index]?.toString())
330330
.toolCalls(buildToolCalls(index))
331331
.build()
332332

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.openai.helpers
2+
3+
import com.openai.models.chat.completions.ChatCompletionChunk
4+
import org.assertj.core.api.Assertions.assertThat
5+
import org.junit.jupiter.api.Test
6+
7+
internal class FragmentedCompletionTextTest {
8+
@Test
9+
fun preservesContentAndRefusalAcrossManyFragments() {
10+
val accumulator = ChatCompletionAccumulator.create()
11+
val delta = ChatCompletionChunk.Choice.Delta.builder().content("aλ😀").refusal("no").build()
12+
val fragment = chunk(delta, false)
13+
repeat(10_000) { accumulator.accumulate(fragment) }
14+
accumulator.accumulate(chunk(ChatCompletionChunk.Choice.Delta.builder().build(), true))
15+
val message = accumulator.chatCompletion().choices().single().message()
16+
assertThat(message.content()).hasValue("aλ😀".repeat(10_000))
17+
assertThat(message.refusal()).hasValue("no".repeat(10_000))
18+
}
19+
20+
@Test
21+
fun distinguishesEmptyContentFromMissingRefusal() {
22+
val accumulator = ChatCompletionAccumulator.create()
23+
accumulator.accumulate(
24+
chunk(ChatCompletionChunk.Choice.Delta.builder().content("").build(), false)
25+
)
26+
accumulator.accumulate(chunk(ChatCompletionChunk.Choice.Delta.builder().build(), true))
27+
val message = accumulator.chatCompletion().choices().single().message()
28+
assertThat(message.content()).hasValue("")
29+
assertThat(message.refusal()).isEmpty()
30+
}
31+
32+
private fun chunk(delta: ChatCompletionChunk.Choice.Delta, finished: Boolean) =
33+
ChatCompletionChunk.builder()
34+
.id("test")
35+
.created(1)
36+
.model("test")
37+
.addChoice(
38+
ChatCompletionChunk.Choice.builder()
39+
.index(0)
40+
.delta(delta)
41+
.finishReason(
42+
if (finished) ChatCompletionChunk.Choice.FinishReason.STOP else null
43+
)
44+
.build()
45+
)
46+
.build()
47+
}

0 commit comments

Comments
 (0)