Skip to content

Commit d466bdc

Browse files
KG-159: Make parts field nullable because Gemini models sometimes don't return it (#652)
1 parent 8779986 commit d466bdc

File tree

4 files changed

+78
-2
lines changed

4 files changed

+78
-2
lines changed

prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ kotlin {
5050
jvmTest {
5151
dependencies {
5252
implementation(kotlin("test-junit5"))
53+
implementation(libs.ktor.client.mock)
5354
}
5455
}
5556
}

prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/DataModel.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ internal class GoogleRequest(
4444
*/
4545
@Serializable
4646
internal class GoogleContent(
47-
val parts: List<GooglePart>,
47+
val parts: List<GooglePart>? = null,
4848
val role: String? = null,
4949
)
5050

prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/commonMain/kotlin/ai/koog/prompt/executor/clients/google/GoogleLLMClient.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ public open class GoogleLLMClient(
220220

221221
val request = createGoogleRequest(prompt, model, tools)
222222

223-
return withContext(Dispatchers.SuitableForIO) {
223+
val response = withContext(Dispatchers.SuitableForIO) {
224224
val response = httpClient.post("$DEFAULT_PATH/${model.id}:$DEFAULT_METHOD_GENERATE_CONTENT") {
225225
setBody(request)
226226
}
@@ -233,6 +233,13 @@ public open class GoogleLLMClient(
233233
error("Error from GoogleAI API: ${response.status}: $errorBody")
234234
}
235235
}
236+
237+
// https://discuss.ai.google.dev/t/gemini-2-5-pro-with-empty-response-text/81175/219
238+
if (response.candidates.isNotEmpty() && response.candidates.all { it.content?.parts?.isEmpty() == true }) {
239+
logger.warn { "Content `parts` field is missing in the response from GoogleAI API: $response" }
240+
}
241+
242+
return response
236243
}
237244

238245
/**

prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src/jvmTest/kotlin/ai/koog/prompt/executor/clients/google/GoogleModelsTest.kt

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,49 @@
11
package ai.koog.prompt.executor.clients.google
22

3+
import ai.koog.prompt.dsl.prompt
34
import ai.koog.prompt.executor.clients.list
45
import ai.koog.prompt.llm.LLMProvider
6+
import ai.koog.prompt.message.Message
7+
import io.ktor.client.HttpClient
8+
import io.ktor.client.engine.mock.MockEngine
9+
import io.ktor.client.engine.mock.respond
10+
import io.ktor.http.HttpHeaders
11+
import io.ktor.http.HttpStatusCode
12+
import io.ktor.http.headersOf
13+
import io.ktor.utils.io.ByteReadChannel
14+
import kotlinx.coroutines.test.runTest
515
import kotlin.test.Test
16+
import kotlin.test.assertEquals
617
import kotlin.test.assertSame
718

19+
// "Bad" request from Gemini with missing `parts` field
20+
private val badRequest: String = """
21+
{
22+
"candidates": [
23+
{
24+
"content": {
25+
"role": "model"
26+
},
27+
"finishReason": "STOP",
28+
"index": 0
29+
}
30+
],
31+
"usageMetadata": {
32+
"promptTokenCount": 36,
33+
"totalTokenCount": 146,
34+
"promptTokensDetails": [
35+
{
36+
"modality": "TEXT",
37+
"tokenCount": 36
38+
}
39+
],
40+
"thoughtsTokenCount": 110
41+
},
42+
"modelVersion": "gemini-2.5-pro",
43+
"responseId": "B0esaJmqKv-0xN8P-dzlwQY"
44+
}
45+
""".trimIndent()
46+
847
class GoogleModelsTest {
948

1049
@Test
@@ -19,4 +58,33 @@ class GoogleModelsTest {
1958
)
2059
}
2160
}
61+
62+
@Test
63+
fun `Test when FLASH_2_5 returns no parts GoogleLLMClient does not fail`() = runTest {
64+
val mockEngine = MockEngine { request ->
65+
respond(
66+
content = ByteReadChannel(badRequest),
67+
status = HttpStatusCode.OK,
68+
headers = headersOf(HttpHeaders.ContentType, "application/json")
69+
)
70+
}
71+
72+
val googleClient = GoogleLLMClient(
73+
apiKey = "test-key",
74+
baseClient = HttpClient(mockEngine) // Ktor client would always respond with the json from above
75+
)
76+
77+
val responses = googleClient.execute(
78+
prompt = prompt("test") { user("What is the capital of France?") },
79+
model = GoogleModels.Gemini2_5Flash
80+
)
81+
82+
assertEquals(1, responses.size)
83+
// When no parts returned -- content should be interpreted as empty
84+
assertEquals("", responses.single().content)
85+
// Also let's check some other fields parsing
86+
assertEquals(Message.Role.Assistant, responses.single().role)
87+
assertEquals(36, responses.single().metaInfo.inputTokensCount)
88+
assertEquals(146, responses.single().metaInfo.totalTokensCount)
89+
}
2290
}

0 commit comments

Comments
 (0)