From a6cf133c50a7fef22d3424af95b161452bf92535 Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Fri, 16 Oct 2026 19:16:08 -0400 Subject: [PATCH] Python: fix(anthropic): map every Anthropic stop reason instead of raising KeyError ANTHROPIC_TO_SEMANTIC_KERNEL_FINISH_REASON_MAP only covered end_turn, max_tokens and tool_use and was indexed with [], so a response that ended with stop_sequence (any request with stop_sequences), refusal, pause_turn or model_context_window_exceeded raised KeyError in both the streaming and the non-streaming path. Map stop_sequence to STOP, model_context_window_exceeded to LENGTH and refusal to CONTENT_FILTER, and look the value up with .get so a stop reason without an equivalent (pause_turn, or a future one) leaves the finish reason unset instead of failing the call. Co-Authored-By: Claude Opus 5.5 --- .../services/anthropic_chat_completion.py | 7 +++- .../test_anthropic_chat_completion.py | 41 ++++++++++++++++++- 2 files changed, 45 insertions(+), 3 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..5539b7ecd20e 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 @@ -61,8 +61,11 @@ # map finish reasons from Anthropic to Semantic Kernel ANTHROPIC_TO_SEMANTIC_KERNEL_FINISH_REASON_MAP = { "end_turn": SemanticKernelFinishReason.STOP, + "stop_sequence": SemanticKernelFinishReason.STOP, "max_tokens": SemanticKernelFinishReason.LENGTH, + "model_context_window_exceeded": SemanticKernelFinishReason.LENGTH, "tool_use": SemanticKernelFinishReason.TOOL_CALLS, + "refusal": SemanticKernelFinishReason.CONTENT_FILTER, } logger: logging.Logger = logging.getLogger(__name__) @@ -270,7 +273,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 +311,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}} 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..34603be89ada 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, 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,40 @@ def test_chat_completion_reset_settings( assert settings.tools is None assert settings.tool_choice is None + + +@pytest.mark.parametrize( + ("stop_reason", "finish_reason"), + [ + ("end_turn", FinishReason.STOP), + ("stop_sequence", FinishReason.STOP), + ("max_tokens", FinishReason.LENGTH), + ("model_context_window_exceeded", FinishReason.LENGTH), + ("tool_use", FinishReason.TOOL_CALLS), + ("refusal", FinishReason.CONTENT_FILTER), + ("pause_turn", None), + ], +) +def test_stop_reason_maps_to_finish_reason(anthropic_unit_test_env, stop_reason, finish_reason) -> None: + chat_completion = AnthropicChatCompletion() + message = Message( + id="msg_1", + type="message", + role="assistant", + model="claude-sonnet-4-5", + content=[TextBlock(type="text", text="Hello")], + stop_reason=stop_reason, + stop_sequence=None, + usage=Usage(input_tokens=1, output_tokens=1), + ) + delta_event = RawMessageDeltaEvent( + type="message_delta", + delta=Delta(stop_reason=stop_reason, stop_sequence=None), + usage=MessageDeltaUsage(output_tokens=1), + ) + + content = chat_completion._create_chat_message_content(message, {}) + streaming_content = chat_completion._create_streaming_chat_message_content(delta_event) + + assert content.finish_reason == finish_reason + assert streaming_content.finish_reason == finish_reason