Skip to content

Commit fb51ef2

Browse files
Fix streaming finishReason in OpenAiChatModel
Intermediate streaming chunks set `ChatGenerationMetadata.finishReason` to `"_UNKNOWN"` instead of `null`. This happened because `chunkToChatCompletion()` uses `FinishReason.of("")` as a placeholder to satisfy the non-streaming builder, which the SDK resolves to the internal `_UNKNOWN` value. Fix this by mapping `_UNKNOWN` to `null` in `buildGeneration()`. Resolves #6625 Closes #6634 Signed-off-by: Ilayaperumal Gopinathan <ilayaperumal.gopinathan@broadcom.com>
1 parent d1e1e6c commit fb51ef2

3 files changed

Lines changed: 38 additions & 1 deletion

File tree

models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,8 +384,10 @@ private Generation buildGeneration(ChatCompletion.Choice choice, Map<String, Obj
384384
}).filter(Objects::nonNull).toList())
385385
.orElse(List.of());
386386

387+
ChatCompletion.Choice.FinishReason.Value finishReasonValue = choice.finishReason().value();
387388
var generationMetadataBuilder = ChatGenerationMetadata.builder()
388-
.finishReason(choice.finishReason().value().name());
389+
.finishReason(finishReasonValue != ChatCompletion.Choice.FinishReason.Value._UNKNOWN
390+
? finishReasonValue.name() : null);
389391

390392
String textContent = message.content().orElse("");
391393

models/spring-ai-openai/src/test/java/org/springframework/ai/openai/OpenAiChatModelTests.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,4 +1383,20 @@ void jsonSchemaResponseFormatStrictCanBeDisabled() {
13831383
assertThat(request.responseFormat().get().jsonSchema().get().jsonSchema().strict()).contains(false);
13841384
}
13851385

1386+
@Test
1387+
void intermediateStreamingChunksHaveNullFinishReason() {
1388+
List<ChatCompletionChunk> chunks = List.of(
1389+
streamingChunk(delta -> delta.role(ChatCompletionChunk.Choice.Delta.Role.ASSISTANT).content("Hello"),
1390+
null),
1391+
streamingChunk(delta -> delta.content(", "), null),
1392+
streamingChunk(delta -> delta.content("world!"), ChatCompletionChunk.Choice.FinishReason.STOP));
1393+
1394+
List<ChatResponse> responses = streamResponses(chunks).collectList().block();
1395+
1396+
assertThat(responses).hasSize(3);
1397+
assertThat(responses.get(0).getResult().getMetadata().getFinishReason()).isNull();
1398+
assertThat(responses.get(1).getResult().getMetadata().getFinishReason()).isNull();
1399+
assertThat(responses.get(2).getResult().getMetadata().getFinishReason()).isEqualTo("STOP");
1400+
}
1401+
13861402
}

models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelIT.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,25 @@ void ensureChatResponseAsContentDoesNotSwallowBlankSpace() throws InterruptedExc
211211
assertThat(answer).contains("1st ");
212212
}
213213

214+
@Test
215+
void intermediateStreamingChunksHaveNullFinishReason() {
216+
Flux<ChatResponse> flux = this.chatModel.stream(new Prompt("List the days of the week, one per line."));
217+
List<ChatResponse> withResult = flux.collectList()
218+
.block()
219+
.stream()
220+
// Only consider chunks that carry a generation — some trailing chunks are
221+
// empty usage-only chunks with no result
222+
.filter(r -> r.getResult() != null)
223+
.collect(Collectors.toList());
224+
225+
assertThat(withResult).hasSizeGreaterThan(1);
226+
// All intermediate chunks must have a null finish reason
227+
withResult.subList(0, withResult.size() - 1)
228+
.forEach(r -> assertThat(r.getResult().getMetadata().getFinishReason()).isNull());
229+
// The final chunk must carry a non-null finish reason (e.g. "STOP")
230+
assertThat(withResult.get(withResult.size() - 1).getResult().getMetadata().getFinishReason()).isNotNull();
231+
}
232+
214233
@Test
215234
void streamRoleTest() {
216235
UserMessage userMessage = new UserMessage(

0 commit comments

Comments
 (0)