Skip to content
Merged
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 @@ -28,6 +28,7 @@
import com.anthropic.models.messages.MessageParam;
import com.anthropic.models.messages.Model;
import com.anthropic.models.messages.OutputConfig;
import com.anthropic.models.messages.StopReason;
import com.anthropic.models.messages.TextBlockParam;
import com.anthropic.models.messages.Tool;
import com.anthropic.models.messages.ToolResultBlockParam;
Expand Down Expand Up @@ -769,10 +770,21 @@ ChatMessage convertResponse(BuiltRequest built, Message response) {
if (!toolCalls.isEmpty()) {
chatMessage.setToolCalls(toolCalls);
}
response.stopReason()
.ifPresent(
reason ->
chatMessage
.getExtraArgs()
.put("finish_reason", toFinishReason(reason)));

return chatMessage;
}

/** Maps Anthropic's token-limit reason to the shared chat action's canonical value. */
private static String toFinishReason(StopReason reason) {
return StopReason.MAX_TOKENS.equals(reason) ? "length" : reason.asString();
}

/**
* Extracts JSON content from a string that may contain markdown code blocks.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import com.anthropic.models.messages.MessageParam;
import com.anthropic.models.messages.Model;
import com.anthropic.models.messages.OutputConfig;
import com.anthropic.models.messages.StopReason;
import com.anthropic.models.messages.TextBlock;
import com.anthropic.models.messages.Usage;
import com.fasterxml.jackson.annotation.JsonSubTypes;
Expand Down Expand Up @@ -105,6 +106,10 @@ private static List<ChatMessage> userMessage() {

/** An assistant response carrying a single text block. */
private static Message textResponse(String text) {
return textResponse(text, Optional.empty());
}

private static Message textResponse(String text, Optional<StopReason> stopReason) {
Usage usage =
Usage.builder()
.inputTokens(1)
Expand All @@ -119,12 +124,43 @@ private static Message textResponse(String text) {
.id("msg_test")
.model(Model.of("claude-sonnet-4-20250514"))
.addContent(TextBlock.builder().text(text).citations(Optional.empty()).build())
.stopReason(Optional.empty())
.stopReason(stopReason)
.stopSequence(Optional.empty())
.usage(usage)
.build();
}

@ParameterizedTest
@MethodSource("anthropicFinishReasons")
@DisplayName("Anthropic response records a finish reason for the shared chat action")
void testResponseRecordsFinishReason(StopReason stopReason, String expectedFinishReason) {
ChatMessage response =
connection()
.convertResponse(
new AnthropicChatModelConnection.BuiltRequest(null, false),
textResponse("partial", Optional.of(stopReason)));

assertThat(response.getExtraArgs()).containsEntry("finish_reason", expectedFinishReason);
}

private static Stream<Arguments> anthropicFinishReasons() {
return Stream.of(
Arguments.of(StopReason.MAX_TOKENS, "length"),
Arguments.of(StopReason.END_TURN, "end_turn"));
}

@Test
@DisplayName("Anthropic response without a stop reason keeps the existing metadata shape")
void testResponseOmitsFinishReasonWhenAbsent() {
ChatMessage response =
connection()
.convertResponse(
new AnthropicChatModelConnection.BuiltRequest(null, false),
textResponse("complete"));

assertThat(response.getExtraArgs()).doesNotContainKey("finish_reason");
}

/** True when the built request ends with the prefilled assistant "{" message. */
private static boolean requestCarriesPrefill(AnthropicChatModelConnection.BuiltRequest built) {
List<MessageParam> messages = built.params.messages();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,13 @@ def chat(
extra_args["promptTokens"] = message.usage.input_tokens
extra_args["completionTokens"] = message.usage.output_tokens

if message.stop_reason is not None:
# The shared chat action recognizes the OpenAI-compatible ``length`` value.
# Anthropic calls the same terminal condition ``max_tokens``.
extra_args["finish_reason"] = (
"length" if message.stop_reason == "max_tokens" else message.stop_reason
)

# A response may lead with a non-text block (e.g. a tool_use block when
# the model calls a tool without any preface), so pick the first text
# block instead of assuming content[0] is text.
Expand Down Expand Up @@ -532,8 +539,6 @@ def chat(
extra_args=extra_args,
)
else:
# TODO: handle other stop_reason values according to Anthropic API:
# https://docs.anthropic.com/en/api/messages#response-stop-reason
return ChatMessage(
role=MessageRole(message.role),
content=text,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,48 @@ def test_plain_text_response_keeps_token_usage() -> None:
assert response.extra_args["completionTokens"] == 3


@pytest.mark.parametrize(
("stop_reason", "finish_reason"),
[("max_tokens", "length"), ("end_turn", "end_turn")],
)
def test_response_records_finish_reason(stop_reason: str, finish_reason: str) -> None:
"""The shared action receives the canonical truncation reason from Anthropic."""
message = Message(
id="m",
model="claude",
role="assistant",
type="message",
stop_reason=stop_reason,
content=[TextBlock(type="text", text="Hello!")],
usage=_usage(),
)

response = _connection_returning(message).chat(
[ChatMessage(role=MessageRole.USER, content="hi")]
)

assert response.extra_args["finish_reason"] == finish_reason


def test_response_omits_finish_reason_when_absent() -> None:
"""A response without a provider stop reason keeps the existing metadata shape."""
message = Message(
id="m",
model="claude",
role="assistant",
type="message",
stop_reason=None,
content=[TextBlock(type="text", text="Hello!")],
usage=_usage(),
)

response = _connection_returning(message).chat(
[ChatMessage(role=MessageRole.USER, content="hi")]
)

assert "finish_reason" not in response.extra_args


def test_tool_use_response_keeps_token_usage() -> None:
# Regression guard for the tool_use path, which already carried usage.
message = Message(
Expand Down