This is an automated email from the ASF dual-hosted git repository.
wenjin272 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/flink-agents.git
The following commit(s) were added to refs/heads/main by this push:
new 0971edc5f [integrations][anthropic][java][python] Preserve Anthropic
finish reason (#1107)
0971edc5f is described below
commit 0971edc5f4f2f0ed81d3e7a92008b763a814d734
Author: Yifan Chen <[email protected]>
AuthorDate: Sun Sep 13 23:00:51 2026 -0700
[integrations][anthropic][java][python] Preserve Anthropic finish reason
(#1107)
Map Anthropic max_tokens responses to the shared length finish reason in
both SDK integrations, while preserving other stop reasons.
Generated-by: Codex CLI 0.153.4 (GPT-6)
---
.../anthropic/AnthropicChatModelConnection.java | 12 +++++++
.../AnthropicChatModelConnectionTest.java | 38 +++++++++++++++++++-
.../chat_models/anthropic/anthropic_chat_model.py | 9 +++--
.../tests/test_anthropic_response_parsing.py | 42 ++++++++++++++++++++++
4 files changed, 98 insertions(+), 3 deletions(-)
diff --git
a/integrations/chat-models/anthropic/src/main/java/org/apache/flink/agents/integrations/chatmodels/anthropic/AnthropicChatModelConnection.java
b/integrations/chat-models/anthropic/src/main/java/org/apache/flink/agents/integrations/chatmodels/anthropic/AnthropicChatModelConnection.java
index e2dcb9831..8b613b02f 100644
---
a/integrations/chat-models/anthropic/src/main/java/org/apache/flink/agents/integrations/chatmodels/anthropic/AnthropicChatModelConnection.java
+++
b/integrations/chat-models/anthropic/src/main/java/org/apache/flink/agents/integrations/chatmodels/anthropic/AnthropicChatModelConnection.java
@@ -28,6 +28,7 @@ import com.anthropic.models.messages.MessageCreateParams;
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;
@@ -769,10 +770,21 @@ public class AnthropicChatModelConnection extends
BaseChatModelConnection {
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.
*
diff --git
a/integrations/chat-models/anthropic/src/test/java/org/apache/flink/agents/integrations/chatmodels/anthropic/AnthropicChatModelConnectionTest.java
b/integrations/chat-models/anthropic/src/test/java/org/apache/flink/agents/integrations/chatmodels/anthropic/AnthropicChatModelConnectionTest.java
index e1e2057b5..daeb10849 100644
---
a/integrations/chat-models/anthropic/src/test/java/org/apache/flink/agents/integrations/chatmodels/anthropic/AnthropicChatModelConnectionTest.java
+++
b/integrations/chat-models/anthropic/src/test/java/org/apache/flink/agents/integrations/chatmodels/anthropic/AnthropicChatModelConnectionTest.java
@@ -23,6 +23,7 @@ import com.anthropic.models.messages.MessageCreateParams;
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;
@@ -105,6 +106,10 @@ class AnthropicChatModelConnectionTest {
/** 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)
@@ -119,12 +124,43 @@ class AnthropicChatModelConnectionTest {
.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();
diff --git
a/python/flink_agents/integrations/chat_models/anthropic/anthropic_chat_model.py
b/python/flink_agents/integrations/chat_models/anthropic/anthropic_chat_model.py
index f4406cc12..0564204ce 100644
---
a/python/flink_agents/integrations/chat_models/anthropic/anthropic_chat_model.py
+++
b/python/flink_agents/integrations/chat_models/anthropic/anthropic_chat_model.py
@@ -495,6 +495,13 @@ class
AnthropicChatModelConnection(BaseChatModelConnection):
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.
@@ -532,8 +539,6 @@ class AnthropicChatModelConnection(BaseChatModelConnection):
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,
diff --git
a/python/flink_agents/integrations/chat_models/anthropic/tests/test_anthropic_response_parsing.py
b/python/flink_agents/integrations/chat_models/anthropic/tests/test_anthropic_response_parsing.py
index 3a492ad35..33466cea2 100644
---
a/python/flink_agents/integrations/chat_models/anthropic/tests/test_anthropic_response_parsing.py
+++
b/python/flink_agents/integrations/chat_models/anthropic/tests/test_anthropic_response_parsing.py
@@ -134,6 +134,48 @@ def test_plain_text_response_keeps_token_usage() -> None:
assert response.extra_args["completionTokens"] == 3
[email protected](
+ ("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(