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(

Reply via email to