diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index b93d40738ea..66fec34e6c1 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -3098,7 +3098,7 @@ class AmazonConverseConfig(BaseConfig): initial_finish_reason: Final = ( "stop" if resolved_json_mode and not filtered_tools and tools and mapped_finish_reason == "tool_calls" - else mapped_finish_reason + else completion_response["stopReason"] ) ( diff --git a/litellm/llms/bedrock/chat/invoke_handler.py b/litellm/llms/bedrock/chat/invoke_handler.py index bc2ef2441d2..45759119b3c 100644 --- a/litellm/llms/bedrock/chat/invoke_handler.py +++ b/litellm/llms/bedrock/chat/invoke_handler.py @@ -711,6 +711,7 @@ class AWSEventStreamDecoder: tool_use: ChatCompletionToolCallChunk | None = None finish_reason = "" usage: Usage | None = None + raw_stop_reason: Final[object] = chunk_data.get("stopReason") provider_specific_fields: dict = {} reasoning_content: str | None = None thinking_blocks: list[ChatCompletionThinkingBlock | ChatCompletionRedactedThinkingBlock] | None = None @@ -734,8 +735,8 @@ class AWSEventStreamDecoder: ) = self._handle_converse_delta_event(delta_obj, content_block_index) elif "contentBlockIndex" in chunk_data: # stop block, no 'start' or 'delta' object tool_use = self._handle_converse_stop_event(content_block_index) - elif "stopReason" in chunk_data: - finish_reason = map_finish_reason(chunk_data.get("stopReason", "stop")) + elif isinstance(raw_stop_reason, str): + finish_reason = map_finish_reason(raw_stop_reason) self._provider_reasoning_tokens = AmazonConverseConfig.thinking_tokens_from_additional_fields( chunk_data.get("additionalModelResponseFields") ) @@ -749,6 +750,12 @@ class AWSEventStreamDecoder: self._thinking_ran = True trace: Final = chunk_data.get("trace") + choice_provider_specific_fields: Final[dict[str, str] | None] = ( + {"native_finish_reason": raw_stop_reason} # mutable-ok: response field contract requires a dict + if isinstance(raw_stop_reason, str) and raw_stop_reason != finish_reason + else None + ) + carries_message_content: Final = bool(trace) or any( key in chunk_data for key in ("start", "delta", "contentBlockIndex", "stopReason") ) @@ -759,6 +766,7 @@ class AWSEventStreamDecoder: StreamingChoices( finish_reason=finish_reason, index=0, # Always 0 - Bedrock never returns multiple choices + provider_specific_fields=choice_provider_specific_fields, delta=Delta( content=text if carries_message_content else None, role="assistant" if carries_message_content else None, diff --git a/tests/unit/llms/bedrock/chat/test_converse_transformation.py b/tests/unit/llms/bedrock/chat/test_converse_transformation.py index b37d6977c10..02e1a1eb56c 100644 --- a/tests/unit/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/unit/llms/bedrock/chat/test_converse_transformation.py @@ -29,6 +29,7 @@ from litellm.llms.anthropic.chat.transformation import ( ) from litellm.llms.bedrock.base_aws_llm import BaseAWSLLM from litellm.llms.bedrock.chat.converse_transformation import AmazonConverseConfig +from litellm.llms.bedrock.chat.invoke_handler import AWSEventStreamDecoder from litellm.llms.bedrock.common_utils import BEDROCK_NATIVE_OUTPUT_MAX_PROPERTIES from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.types.llms.bedrock import ContentBlock, ConverseTokenUsageBlock, RequestObject @@ -5966,6 +5967,59 @@ def test_transform_response_native_structured_output(): assert result.choices[0].finish_reason == "stop" +@pytest.mark.parametrize("stop_reason", ("stop_sequence", "end_turn")) +def test_transform_response_preserves_raw_bedrock_stop_reason(stop_reason: str): + response_json = { + "metrics": {"latencyMs": 1}, + "output": { + "message": { + "role": "assistant", + "content": [{"text": "done"}], + } + }, + "stopReason": stop_reason, + "usage": { + "inputTokens": 10, + "outputTokens": 1, + "totalTokens": 11, + }, + } + raw_response = httpx.Response( + 200, + json=response_json, + request=httpx.Request("POST", "https://bedrock.test/converse"), + ) + + result = AmazonConverseConfig().transform_response( + model="bedrock/us.anthropic.claude-sonnet-4-5-20250929-v1:0", + raw_response=raw_response, + model_response=ModelResponse(), + logging_obj=None, + request_data={}, + messages=[], + optional_params={}, + litellm_params={}, + encoding=None, + ) + + assert result.choices[0].finish_reason == "stop" + assert result.choices[0].provider_specific_fields == { + "native_finish_reason": stop_reason + } + + +@pytest.mark.parametrize("stop_reason", ("stop_sequence", "end_turn")) +def test_converse_chunk_parser_preserves_raw_bedrock_stop_reason(stop_reason: str): + result = AWSEventStreamDecoder(model="bedrock/claude").converse_chunk_parser( + {"stopReason": stop_reason} + ) + + assert result.choices[0].finish_reason == "stop" + assert result.choices[0].provider_specific_fields == { + "native_finish_reason": stop_reason + } + + def test_add_additional_properties_simple_object(): """Object schemas without additionalProperties get it set to false.""" schema = {