From c04f0c7a04a95d7e50f48997e85b420bf78530a8 Mon Sep 17 00:00:00 2001 From: Silu Panda <31051721+SiluPanda@users.noreply.github.com> Date: Sun, 30 Aug 2026 05:48:56 -0700 Subject: [PATCH] fix: preserve response input message roundtrips --- .../models/responses/EasyInputMessage.kt | 2 +- .../models/responses/ResponseInputItem.kt | 31 +++++-- .../models/responses/ResponseInputItemTest.kt | 92 +++++++++++++++++++ 3 files changed, 116 insertions(+), 9 deletions(-) diff --git a/openai-java-core/src/main/kotlin/com/openai/models/responses/EasyInputMessage.kt b/openai-java-core/src/main/kotlin/com/openai/models/responses/EasyInputMessage.kt index 85cda3a97..0389ae5ef 100644 --- a/openai-java-core/src/main/kotlin/com/openai/models/responses/EasyInputMessage.kt +++ b/openai-java-core/src/main/kotlin/com/openai/models/responses/EasyInputMessage.kt @@ -150,7 +150,7 @@ private constructor( private var content: JsonField? = null private var role: JsonField? = null private var phase: JsonField = JsonMissing.of() - private var type: JsonField = JsonMissing.of() + private var type: JsonField = JsonField.of(Type.MESSAGE) private var additionalProperties: MutableMap = mutableMapOf() @JvmSynthetic diff --git a/openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseInputItem.kt b/openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseInputItem.kt index 1b30d84bc..502146cac 100644 --- a/openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseInputItem.kt +++ b/openai-java-core/src/main/kotlin/com/openai/models/responses/ResponseInputItem.kt @@ -1218,18 +1218,33 @@ private constructor( override fun ObjectCodec.deserialize(node: JsonNode): ResponseInputItem { val json = JsonValue.fromJsonNode(node) - val type = json.asObject().getOrNull()?.get("type")?.asString()?.getOrNull() + val jsonObject = json.asObject().getOrNull() + val type = jsonObject?.get("type")?.asString()?.getOrNull() when (type) { "message" -> { + val role = jsonObject?.get("role")?.asString()?.getOrNull() + val hasPhase = jsonObject?.containsKey("phase") == true + val hasStructuredContent = + jsonObject?.get("content")?.asArray()?.getOrNull() != null + val isMessageRole = role == "user" || role == "system" || role == "developer" + // These variants share a discriminator and become wire-identical in this + // shape. Prefer the narrower Message; string content, assistant roles, and + // phase-bearing messages remain EasyInputMessage. + val preferMessage = hasStructuredContent && isMessageRole && !hasPhase + val messageMatch = + tryDeserialize(node, jacksonTypeRef())?.let { + ResponseInputItem(message = it, _json = json) + } + val easyInputMessageMatch = + tryDeserialize(node, jacksonTypeRef())?.let { + ResponseInputItem(easyInputMessage = it, _json = json) + } + val bestMatches = sequenceOf( - tryDeserialize(node, jacksonTypeRef())?.let { - ResponseInputItem(easyInputMessage = it, _json = json) - }, - tryDeserialize(node, jacksonTypeRef())?.let { - ResponseInputItem(message = it, _json = json) - }, + if (preferMessage) messageMatch else easyInputMessageMatch, + if (preferMessage) easyInputMessageMatch else messageMatch, tryDeserialize(node, jacksonTypeRef())?.let { ResponseInputItem(responseOutputMessage = it, _json = json) }, @@ -1572,7 +1587,7 @@ private constructor( private var content: JsonField>? = null private var role: JsonField? = null private var status: JsonField = JsonMissing.of() - private var type: JsonField = JsonMissing.of() + private var type: JsonField = JsonField.of(Type.MESSAGE) private var additionalProperties: MutableMap = mutableMapOf() @JvmSynthetic diff --git a/openai-java-core/src/test/kotlin/com/openai/models/responses/ResponseInputItemTest.kt b/openai-java-core/src/test/kotlin/com/openai/models/responses/ResponseInputItemTest.kt index fca76ee4c..6bb30b874 100644 --- a/openai-java-core/src/test/kotlin/com/openai/models/responses/ResponseInputItemTest.kt +++ b/openai-java-core/src/test/kotlin/com/openai/models/responses/ResponseInputItemTest.kt @@ -82,6 +82,26 @@ internal class ResponseInputItemTest { assertThat(roundtrippedResponseInputItem).isEqualTo(responseInputItem) } + @Test + fun ofEasyInputMessageRoundtripWithoutExplicitType() { + val jsonMapper = jsonMapper() + val responseInputItem = + ResponseInputItem.ofEasyInputMessage( + EasyInputMessage.builder() + .content("string") + .role(EasyInputMessage.Role.USER) + .build() + ) + + val roundtrippedResponseInputItem = + jsonMapper.readValue( + jsonMapper.writeValueAsString(responseInputItem), + jacksonTypeRef(), + ) + + assertThat(roundtrippedResponseInputItem).isEqualTo(responseInputItem) + } + @Test fun ofMessage() { val message = @@ -164,6 +184,78 @@ internal class ResponseInputItemTest { assertThat(roundtrippedResponseInputItem).isEqualTo(responseInputItem) } + @Test + fun ofMessageRoundtripWithoutExplicitType() { + val jsonMapper = jsonMapper() + val responseInputItem = + ResponseInputItem.ofMessage( + ResponseInputItem.Message.builder() + .addInputTextContent("Test") + .role(ResponseInputItem.Message.Role.USER) + .build() + ) + + val roundtrippedResponseInputItem = + jsonMapper.readValue( + jsonMapper.writeValueAsString(responseInputItem), + jacksonTypeRef(), + ) + + assertThat(roundtrippedResponseInputItem).isEqualTo(responseInputItem) + } + + @Test + fun ofStructuredAssistantEasyInputMessageRoundtripWithoutPhase() { + val jsonMapper = jsonMapper() + val responseInputItem = + ResponseInputItem.ofEasyInputMessage( + EasyInputMessage.builder() + .contentOfResponseInputMessageContentList( + listOf( + ResponseInputContent.ofInputText( + ResponseInputText.builder().text("text").build() + ) + ) + ) + .role(EasyInputMessage.Role.ASSISTANT) + .type(EasyInputMessage.Type.MESSAGE) + .build() + ) + + val roundtrippedResponseInputItem = + jsonMapper.readValue( + jsonMapper.writeValueAsString(responseInputItem), + jacksonTypeRef(), + ) + + assertThat(roundtrippedResponseInputItem).isEqualTo(responseInputItem) + } + + @Test + fun deserializesStructuredNonAssistantMessagePayloadAsMessage() { + val jsonMapper = jsonMapper() + val responseInputItem = + jsonMapper.readValue( + """ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "text" + } + ] + } + """ + .trimIndent(), + jacksonTypeRef(), + ) + + assertThat(responseInputItem.message()).isPresent + assertThat(responseInputItem.easyInputMessage()).isEmpty + } + @Test fun ofResponseOutputMessage() { val responseOutputMessage =