Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ private constructor(
fun _additionalProperties(): Map<String, JsonValue> =
Collections.unmodifiableMap(additionalProperties)

@JvmSynthetic
internal fun isEmptyAssistant(): Boolean =
_role().asString().orElse(null) == "assistant" &&
_content()
.asKnown()
.map {
it.string().map(String::isEmpty).orElseGet {
it.betaContentBlockParams().map(List<*>::isEmpty).orElse(false)
}
}
.orElse(false)

fun toBuilder() = Builder().from(this)

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1847,7 +1847,13 @@ private constructor(
fun addMessage(message: BetaMessageParam) = apply {
messages =
(messages ?: JsonField.of(mutableListOf())).also {
checkKnown("messages", it).add(message)
checkKnown("messages", it).apply {
// Empty assistant content is only valid as the final message.
if (lastOrNull()?.isEmptyAssistant() == true) {
removeLast()
}
add(message)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2809,7 +2809,13 @@ private constructor(
fun addMessage(message: BetaMessageParam) = apply {
messages =
(messages ?: JsonField.of(mutableListOf())).also {
checkKnown("messages", it).add(message)
checkKnown("messages", it).apply {
// Empty assistant content is only valid as the final message.
if (lastOrNull()?.isEmptyAssistant() == true) {
removeLast()
}
add(message)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1720,7 +1720,13 @@ private constructor(
fun addMessage(message: BetaMessageParam) = apply {
messages =
(messages ?: JsonField.of(mutableListOf())).also {
checkKnown("messages", it).add(message)
checkKnown("messages", it).apply {
// Empty assistant content is only valid as the final message.
if (lastOrNull()?.isEmptyAssistant() == true) {
removeLast()
}
add(message)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1478,7 +1478,13 @@ private constructor(
fun addMessage(message: MessageParam) = apply {
messages =
(messages ?: JsonField.of(mutableListOf())).also {
checkKnown("messages", it).add(message)
checkKnown("messages", it).apply {
// Empty assistant content is only valid as the final message.
if (lastOrNull()?.isEmptyAssistant() == true) {
removeLast()
}
add(message)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2173,7 +2173,13 @@ private constructor(
fun addMessage(message: MessageParam) = apply {
messages =
(messages ?: JsonField.of(mutableListOf())).also {
checkKnown("messages", it).add(message)
checkKnown("messages", it).apply {
// Empty assistant content is only valid as the final message.
if (lastOrNull()?.isEmptyAssistant() == true) {
removeLast()
}
add(message)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ private constructor(
fun _additionalProperties(): Map<String, JsonValue> =
Collections.unmodifiableMap(additionalProperties)

@JvmSynthetic
internal fun isEmptyAssistant(): Boolean =
_role().asString().orElse(null) == "assistant" &&
_content()
.asKnown()
.map {
it.string().map(String::isEmpty).orElseGet {
it.blockParams().map(List<*>::isEmpty).orElse(false)
}
}
.orElse(false)

fun toBuilder() = Builder().from(this)

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1504,7 +1504,13 @@ private constructor(
fun addMessage(message: MessageParam) = apply {
messages =
(messages ?: JsonField.of(mutableListOf())).also {
checkKnown("messages", it).add(message)
checkKnown("messages", it).apply {
// Empty assistant content is only valid as the final message.
if (lastOrNull()?.isEmptyAssistant() == true) {
removeLast()
}
add(message)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.anthropic.models.beta.messages

import com.anthropic.core.jsonMapper
import com.anthropic.models.messages.Model
import com.fasterxml.jackson.module.kotlin.jacksonTypeRef
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

internal class Issue260RoundTripTest {

private val jsonMapper = jsonMapper()

@Test
fun `empty assistant response is omitted before a subsequent request message`() {
val message =
jsonMapper.readValue(
"""{"role":"assistant","content":[]}""",
jacksonTypeRef<BetaMessage>(),
)

val paramsBuilder =
MessageCreateParams.builder()
.model(Model.CLAUDE_SONNET_4_20250514)
.maxTokens(64)
.addUserMessage("first question")
.addMessage(message)

val finalAssistantJson =
jsonMapper.valueToTree<com.fasterxml.jackson.databind.JsonNode>(
paramsBuilder.build()._body()
)

assertThat(finalAssistantJson.at("/messages/1/role").textValue()).isEqualTo("assistant")
assertThat(finalAssistantJson.at("/messages/1/content")).isEmpty()

val followUpJson =
jsonMapper.valueToTree<com.fasterxml.jackson.databind.JsonNode>(
paramsBuilder.addUserMessage("follow-up question").build()._body()
)

assertThat(followUpJson.at("/messages").size()).isEqualTo(2)
assertThat(followUpJson.at("/messages/0/content").textValue()).isEqualTo("first question")
assertThat(followUpJson.at("/messages/1/content").textValue())
.isEqualTo("follow-up question")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package com.anthropic.models.messages

import com.anthropic.core.jsonMapper
import com.fasterxml.jackson.module.kotlin.jacksonTypeRef
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Test

internal class Issue260RoundTripTest {

private val jsonMapper = jsonMapper()

@Test
fun `web search response can be converted back into request params`() {
val message =
readMessage(
"""
{
"role": "assistant",
"content": [{
"type": "web_search_tool_result",
"tool_use_id": "srvtoolu_123",
"caller": {"type": "direct"},
"content": [{
"type": "web_search_result",
"url": "https://example.com",
"title": "Example",
"encrypted_content": "encrypted"
}]
}]
}
"""
)

val json =
jsonMapper.valueToTree<com.fasterxml.jackson.databind.JsonNode>(message.toParam())

assertThat(json.at("/content/0/content/0/encrypted_content").textValue())
.isEqualTo("encrypted")
}

@Test
fun `missing web search content no longer throws while converting to params`() {
val message =
readMessage(
"""
{
"role": "assistant",
"content": [{
"type": "web_search_tool_result",
"tool_use_id": "srvtoolu_123",
"caller": {"type": "direct"}
}]
}
"""
)

val json =
jsonMapper.valueToTree<com.fasterxml.jackson.databind.JsonNode>(message.toParam())

assertThat(json.at("/content/0").has("content")).isFalse()
}

@Test
fun `empty assistant response is omitted before a subsequent request message`() {
val message = readMessage("""{"role":"assistant","content":[]}""")

val paramsBuilder =
MessageCreateParams.builder()
.model(Model.CLAUDE_SONNET_4_20250514)
.maxTokens(64)
.addUserMessage("first question")
.addMessage(message)

val finalAssistantJson =
jsonMapper.valueToTree<com.fasterxml.jackson.databind.JsonNode>(
paramsBuilder.build()._body()
)

assertThat(finalAssistantJson.at("/messages/1/role").textValue()).isEqualTo("assistant")
assertThat(finalAssistantJson.at("/messages/1/content")).isEmpty()

val followUpJson =
jsonMapper.valueToTree<com.fasterxml.jackson.databind.JsonNode>(
paramsBuilder.addUserMessage("follow-up question").build()._body()
)

assertThat(followUpJson.at("/messages").size()).isEqualTo(2)
assertThat(followUpJson.at("/messages/0/content").textValue()).isEqualTo("first question")
assertThat(followUpJson.at("/messages/1/content").textValue())
.isEqualTo("follow-up question")
}

private fun readMessage(json: String): Message =
jsonMapper.readValue(json.trimIndent(), jacksonTypeRef<Message>())
}