Skip to content

Commit d5a5aed

Browse files
Mateusz Krawieccopybara-github
authored andcommitted
fix: omit empty content from an LlmResponse built from a content-less candidate
A candidate with no content parts (for example one blocked for safety) now yields an LlmResponse with null content instead of an empty Content, unless the turn finished normally. PiperOrigin-RevId: 967732960
1 parent 3a824fc commit d5a5aed

2 files changed

Lines changed: 46 additions & 1 deletion

File tree

core/src/commonMain/kotlin/com/google/adk/kt/models/LlmResponse.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,11 @@ data class LlmResponse(
167167
candidate?.finishReason ?: response.promptFeedback?.blockReason?.toFinishReason()
168168

169169
return LlmResponse(
170-
content = candidate?.content,
170+
// Keep content only when it has parts or the turn finished normally, matching Python ADK.
171+
content =
172+
candidate?.content?.takeIf {
173+
it.parts.isNotEmpty() || candidate.finishReason == FinishReason.STOP
174+
},
171175
usageMetadata = response.usageMetadata,
172176
finishReason = finishReason,
173177
errorCode = finishReason?.takeIf { it != FinishReason.STOP }?.name,

core/src/commonTest/kotlin/com/google/adk/kt/models/LlmResponseTest.kt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import com.google.adk.kt.types.GenerateContentResponse
2525
import com.google.adk.kt.types.PromptFeedback
2626
import com.google.adk.kt.types.Role
2727
import kotlin.test.assertEquals
28+
import kotlin.test.assertNotNull
2829
import kotlin.test.assertNull
2930
import org.junit.Test
3031
import org.junit.runner.RunWith
@@ -89,6 +90,46 @@ class LlmResponseTest {
8990
assertEquals("Safety filter triggered", llmResponse.errorMessage)
9091
}
9192

93+
@Test
94+
fun testContentlessCandidateHasNoContent() {
95+
// No parts and not STOP: null content, not an empty Content (they serialize differently).
96+
val response =
97+
GenerateContentResponse(
98+
candidates =
99+
listOf(
100+
Candidate(
101+
content = Content(role = Role.MODEL, parts = emptyList()),
102+
finishReason = FinishReason.SAFETY,
103+
)
104+
)
105+
)
106+
107+
val llmResponse = LlmResponse.from(response)
108+
109+
assertNull(llmResponse.content)
110+
}
111+
112+
@Test
113+
fun testCreateStopWithEmptyPartsKeepsContent() {
114+
// A candidate that finished normally keeps its (empty) content rather than dropping it.
115+
val response =
116+
GenerateContentResponse(
117+
candidates =
118+
listOf(
119+
Candidate(
120+
content = Content(role = Role.MODEL, parts = emptyList()),
121+
finishReason = FinishReason.STOP,
122+
)
123+
)
124+
)
125+
126+
val llmResponse = LlmResponse.from(response)
127+
128+
val content = assertNotNull(llmResponse.content)
129+
assertEquals(0, content.parts.size)
130+
assertNull(llmResponse.errorCode)
131+
}
132+
92133
@Test
93134
fun testCreateNoCandidates() {
94135
val response =

0 commit comments

Comments
 (0)