From 373a95edf32d8698676ff5ab7c581c121fa49e3a Mon Sep 17 00:00:00 2001 From: Shxiao101 Date: Sun, 6 Sep 2026 20:14:51 +0900 Subject: [PATCH 1/2] fix(python): map Anthropic stop_sequence finish reason and fall back to None for unknown reasons stop_sequences in AnthropicChatPromptExecutionSettings is user-settable; when the API returns stop_reason='stop_sequence' the direct dict index raised KeyError (non-streaming) and broke the stream on RawMessageDeltaEvent. Add the mapping and use .get() so unknown reasons degrade to None. --- .../ai/anthropic/services/anthropic_chat_completion.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/python/semantic_kernel/connectors/ai/anthropic/services/anthropic_chat_completion.py b/python/semantic_kernel/connectors/ai/anthropic/services/anthropic_chat_completion.py index a2323a577a69..233015ed2f54 100644 --- a/python/semantic_kernel/connectors/ai/anthropic/services/anthropic_chat_completion.py +++ b/python/semantic_kernel/connectors/ai/anthropic/services/anthropic_chat_completion.py @@ -62,6 +62,7 @@ ANTHROPIC_TO_SEMANTIC_KERNEL_FINISH_REASON_MAP = { "end_turn": SemanticKernelFinishReason.STOP, "max_tokens": SemanticKernelFinishReason.LENGTH, + "stop_sequence": SemanticKernelFinishReason.STOP, "tool_use": SemanticKernelFinishReason.TOOL_CALLS, } @@ -270,7 +271,7 @@ def _create_chat_message_content( finish_reason = None if response.stop_reason: - finish_reason = ANTHROPIC_TO_SEMANTIC_KERNEL_FINISH_REASON_MAP[response.stop_reason] + finish_reason = ANTHROPIC_TO_SEMANTIC_KERNEL_FINISH_REASON_MAP.get(response.stop_reason) return ChatMessageContent( inner_content=response, @@ -308,7 +309,7 @@ def _create_streaming_chat_message_content( ) ) elif isinstance(stream_event, RawMessageDeltaEvent): - finish_reason = ANTHROPIC_TO_SEMANTIC_KERNEL_FINISH_REASON_MAP[str(stream_event.delta.stop_reason)] + finish_reason = ANTHROPIC_TO_SEMANTIC_KERNEL_FINISH_REASON_MAP.get(str(stream_event.delta.stop_reason)) output_tokens = stream_event.usage.output_tokens if metadata is None: metadata = {"usage": {"output_tokens": output_tokens}} From 4967a4d91576a30d2ff74f1e0b3b60da848de8a0 Mon Sep 17 00:00:00 2001 From: Shxiao101 Date: Mon, 7 Sep 2026 08:51:29 +0900 Subject: [PATCH 2/2] test(python): cover Anthropic stop_sequence mapping and unknown stop reason fallback --- .../test_anthropic_chat_completion.py | 111 +++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/python/tests/unit/connectors/ai/anthropic/services/test_anthropic_chat_completion.py b/python/tests/unit/connectors/ai/anthropic/services/test_anthropic_chat_completion.py index bff83bfe89d6..e9ae6a312b8b 100644 --- a/python/tests/unit/connectors/ai/anthropic/services/test_anthropic_chat_completion.py +++ b/python/tests/unit/connectors/ai/anthropic/services/test_anthropic_chat_completion.py @@ -3,7 +3,8 @@ import pytest from anthropic import AsyncAnthropic -from anthropic.types import Message +from anthropic.types import Message, MessageDeltaUsage, RawMessageDeltaEvent, RawMessageStartEvent, TextBlock, Usage +from anthropic.types.raw_message_delta_event import Delta from semantic_kernel.connectors.ai.anthropic.prompt_execution_settings.anthropic_prompt_execution_settings import ( AnthropicChatPromptExecutionSettings, @@ -18,6 +19,7 @@ from semantic_kernel.contents.chat_message_content import ChatMessageContent, FunctionCallContent, TextContent from semantic_kernel.contents.streaming_chat_message_content import StreamingChatMessageContent from semantic_kernel.contents.utils.author_role import AuthorRole +from semantic_kernel.contents.utils.finish_reason import FinishReason from semantic_kernel.exceptions import ( ServiceInitializationError, ServiceInvalidExecutionSettingsError, @@ -547,3 +549,110 @@ def test_chat_completion_reset_settings( assert settings.tools is None assert settings.tool_choice is None + + +@pytest.mark.parametrize( + "stop_reason,expected_finish_reason", + [ + pytest.param("end_turn", FinishReason.STOP, id="end_turn"), + pytest.param("max_tokens", FinishReason.LENGTH, id="max_tokens"), + pytest.param("tool_use", FinishReason.TOOL_CALLS, id="tool_use"), + pytest.param("stop_sequence", FinishReason.STOP, id="stop_sequence"), + pytest.param("refusal", None, id="unknown_stop_reason_returns_none"), + ], +) +async def test_finish_reason_from_stop_reason( + kernel: Kernel, + mock_settings: AnthropicChatPromptExecutionSettings, + stop_reason: str, + expected_finish_reason: FinishReason | None, +): + response = Message( + id="test_message_id", + content=[TextBlock(text="test", type="text")], + model="test_model_id", + role="assistant", + stop_reason=stop_reason, + stop_sequence=None, + type="message", + usage=Usage(input_tokens=10, output_tokens=10), + ) + client = MagicMock(spec=AsyncAnthropic) + messages_mock = MagicMock() + messages_mock.create = AsyncMock(return_value=response) + client.messages = messages_mock + + chat_history = ChatHistory() + chat_history.add_user_message("test_user_message") + + chat_completion_base = AnthropicChatCompletion( + ai_model_id="test_model_id", service_id="test", api_key="", async_client=client + ) + contents: list[ChatMessageContent] = await chat_completion_base.get_chat_message_contents( + chat_history=chat_history, settings=mock_settings, kernel=kernel, arguments=KernelArguments() + ) + + assert contents[0].finish_reason is expected_finish_reason + + +@pytest.mark.parametrize( + "stop_reason,expected_finish_reason", + [ + pytest.param("stop_sequence", FinishReason.STOP, id="stop_sequence"), + pytest.param("refusal", None, id="unknown_stop_reason_returns_none"), + ], +) +async def test_finish_reason_from_stop_reason_stream( + kernel: Kernel, + mock_settings: AnthropicChatPromptExecutionSettings, + stop_reason: str, + expected_finish_reason: FinishReason | None, +): + stream_events = [ + RawMessageStartEvent( + message=Message( + id="test_message_id", + content=[], + model="test_model_id", + role="assistant", + stop_reason=None, + stop_sequence=None, + type="message", + usage=Usage(input_tokens=10, output_tokens=2), + ), + type="message_start", + ), + RawMessageDeltaEvent( + delta=Delta(stop_reason=stop_reason, stop_sequence=None), + type="message_delta", + usage=MessageDeltaUsage(output_tokens=10), + ), + ] + + async def async_generator(): + for event in stream_events: + yield event + + stream_mock = AsyncMock() + stream_mock.__aenter__.return_value = async_generator() + + client = MagicMock(spec=AsyncAnthropic) + messages_mock = MagicMock() + messages_mock.stream.return_value = stream_mock + client.messages = messages_mock + + chat_history = ChatHistory() + chat_history.add_user_message("test_user_message") + + chat_completion_base = AnthropicChatCompletion( + ai_model_id="test_model_id", service_id="test", api_key="", async_client=client + ) + + last_content: StreamingChatMessageContent | None = None + async for content in chat_completion_base.get_streaming_chat_message_contents( + chat_history, mock_settings, kernel=kernel, arguments=KernelArguments() + ): + last_content = content[0] + + assert last_content is not None + assert last_content.finish_reason is expected_finish_reason