Skip to content

Commit 6469a74

Browse files
mltheuserMalte Heuserkpavlov
authored
KG-487 Add handler for GooglePart.InlineData (#1094)
YouTrack link: https://youtrack.jetbrains.com/issue/KG-487/GoogleLLMClient-does-not-support-InlineData-part-type #954 ## Motivation and Context Currently, `GoogleLLMClient` throws an `IllegalStateException: Not supported part type: InlineData` when processing responses from Gemini models that return binary content, such as images (e.g., from `gemini-2.5-flash-image`). This crash occurs because the `processGoogleCandidate` function lacks a handler for the `GooglePart.InlineData` type. This PR adds the necessary logic to correctly parse `InlineData` parts into `Message.Assistant` messages containing either `ContentPart.Image` or `ContentPart.File`, allowing the client to support multimodal responses from Google's API. ## >> Important Note << `gemini-2.5-flash-image` is not in GoogleModels.kt and therefor nor officially supported yet. For that reason there are only unit tests for the `InlineData` handler in this PR. I would be open to add this Model but best to open a separate Issue for it and discuss. ## Breaking Changes None. This change is a non-breaking bug fix that adds support for a previously unhandled response type. Existing functionality is unaffected. Co-authored-by: Malte Heuser <[email protected]> Co-authored-by: Konstantin Pavlov <[email protected]>
1 parent 639fe72 commit 6469a74

File tree

2 files changed

+83
-1
lines changed
  • prompt/prompt-executor/prompt-executor-clients/prompt-executor-google-client/src

2 files changed

+83
-1
lines changed

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

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -601,7 +601,7 @@ public open class GoogleLLMClient(
601601
* @return A list of response messages
602602
*/
603603
@OptIn(ExperimentalUuidApi::class)
604-
private fun processGoogleCandidate(candidate: GoogleCandidate, metaInfo: ResponseMetaInfo): List<Message.Response> {
604+
internal fun processGoogleCandidate(candidate: GoogleCandidate, metaInfo: ResponseMetaInfo): List<Message.Response> {
605605
val parts = candidate.content?.parts.orEmpty()
606606
val responses = mutableListOf<Message.Response>()
607607
with(responses) {
@@ -646,6 +646,29 @@ public open class GoogleLLMClient(
646646
)
647647
)
648648

649+
is GooglePart.InlineData -> {
650+
val inlineData = part.inlineData
651+
val contentPart = when (val mimeType = inlineData.mimeType) {
652+
"image/png", "image/jpeg", "image/webp" -> ContentPart.Image(
653+
content = AttachmentContent.Binary.Bytes(inlineData.data),
654+
format = mimeType.substringAfter("image/"),
655+
mimeType = mimeType,
656+
)
657+
else -> ContentPart.File(
658+
content = AttachmentContent.Binary.Bytes(inlineData.data),
659+
mimeType = mimeType,
660+
format = mimeType.substringAfterLast('.'),
661+
)
662+
}
663+
add(
664+
Message.Assistant(
665+
parts = listOf(contentPart),
666+
finishReason = candidate.finishReason,
667+
metaInfo = metaInfo
668+
)
669+
)
670+
}
671+
649672
else -> error("Not supported part type: $part")
650673
}
651674
}

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,19 @@ import ai.koog.agents.core.tools.ToolDescriptor
44
import ai.koog.agents.core.tools.ToolParameterDescriptor
55
import ai.koog.agents.core.tools.ToolParameterType
66
import ai.koog.prompt.dsl.Prompt
7+
import ai.koog.prompt.executor.clients.google.models.GoogleCandidate
8+
import ai.koog.prompt.executor.clients.google.models.GoogleContent
9+
import ai.koog.prompt.executor.clients.google.models.GoogleData
710
import ai.koog.prompt.executor.clients.google.models.GoogleFunctionCallingMode
11+
import ai.koog.prompt.executor.clients.google.models.GooglePart
812
import ai.koog.prompt.executor.clients.google.models.GoogleThinkingConfig
13+
import ai.koog.prompt.message.AttachmentContent
14+
import ai.koog.prompt.message.ContentPart
15+
import ai.koog.prompt.message.Message
16+
import ai.koog.prompt.message.ResponseMetaInfo
917
import ai.koog.prompt.params.LLMParams
1018
import io.kotest.matchers.collections.shouldContain
19+
import io.kotest.matchers.collections.shouldHaveSize
1120
import io.kotest.matchers.shouldBe
1221
import io.kotest.matchers.shouldNotBe
1322
import kotlinx.serialization.json.JsonObject
@@ -325,4 +334,54 @@ class GoogleLLMClientTest {
325334
fc!!.mode shouldBe GoogleFunctionCallingMode.ANY
326335
fc.allowedFunctionNames shouldBe listOf("weather")
327336
}
337+
338+
@Test
339+
fun `processGoogleCandidate should handle InlineData image part`() {
340+
val client = GoogleLLMClient(apiKey = "apiKey")
341+
val imageData = "png-bytes".encodeToByteArray()
342+
val candidate = GoogleCandidate(
343+
content = GoogleContent(
344+
role = "model",
345+
parts = listOf(
346+
GooglePart.InlineData(
347+
inlineData = GoogleData.Blob("image/png", imageData)
348+
)
349+
)
350+
)
351+
)
352+
353+
val responses = client.processGoogleCandidate(candidate, ResponseMetaInfo.Empty)
354+
355+
responses shouldHaveSize 1
356+
val assistantMessage = responses.single() as Message.Assistant
357+
assistantMessage.parts shouldHaveSize 1
358+
val imagePart = assistantMessage.parts.single() as ContentPart.Image
359+
imagePart.format shouldBe "png"
360+
(imagePart.content as AttachmentContent.Binary.Bytes).asBytes() shouldBe imageData
361+
}
362+
363+
@Test
364+
fun `processGoogleCandidate should handle InlineData generic file part`() {
365+
val client = GoogleLLMClient(apiKey = "apiKey")
366+
val fileData = "pdf-bytes".encodeToByteArray()
367+
val candidate = GoogleCandidate(
368+
content = GoogleContent(
369+
role = "model",
370+
parts = listOf(
371+
GooglePart.InlineData(
372+
inlineData = GoogleData.Blob("application/pdf", fileData)
373+
)
374+
)
375+
)
376+
)
377+
378+
val responses = client.processGoogleCandidate(candidate, ResponseMetaInfo.Empty)
379+
380+
responses shouldHaveSize 1
381+
val assistantMessage = responses.single() as Message.Assistant
382+
assistantMessage.parts shouldHaveSize 1
383+
val filePart = assistantMessage.parts.single() as ContentPart.File
384+
filePart.mimeType shouldBe "application/pdf"
385+
(filePart.content as AttachmentContent.Binary.Bytes).asBytes() shouldBe fileData
386+
}
328387
}

0 commit comments

Comments
 (0)