diff --git a/litellm/litellm_core_utils/streaming_chunk_builder_utils.py b/litellm/litellm_core_utils/streaming_chunk_builder_utils.py index 45c8565e62f..88d3b42dcaf 100644 --- a/litellm/litellm_core_utils/streaming_chunk_builder_utils.py +++ b/litellm/litellm_core_utils/streaming_chunk_builder_utils.py @@ -464,6 +464,14 @@ class ChunkProcessor: None, ) + @staticmethod + def _choice_provider_specific_fields(chunk: "_BaseChunk") -> Mapping[str, object]: + choices: Final = chunk.get("choices") + if not choices: + return MappingProxyType({}) + fields: Final = choices[0].get("provider_specific_fields") + return fields if isinstance(fields, dict) else MappingProxyType({}) + def build_base_response( self, chunks: Sequence["_BaseChunk"], @@ -482,14 +490,9 @@ class ChunkProcessor: role: Final = ChunkProcessor._get_role_from_chunks(chunks) finish_reason: Final = ChunkProcessor._finish_reason_of_chunks(chunks) - choice_provider_specific_fields: Final[dict[str, object]] = { # mutable-ok: response field requires a dict - key: value - for chunk in chunks - if chunk.get("choices") - for fields in (chunk["choices"][0].get("provider_specific_fields"),) - if isinstance(fields, dict) - for key, value in fields.items() - } + choice_provider_specific_fields: Final[dict[str, object]] = dict( # mutable-ok: response field requires a dict + chain.from_iterable(ChunkProcessor._choice_provider_specific_fields(chunk).items() for chunk in chunks) + ) # Initialize the response dictionary response = ModelResponse( diff --git a/litellm/llms/bedrock/chat/converse_transformation.py b/litellm/llms/bedrock/chat/converse_transformation.py index 66fec34e6c1..771897483a4 100644 --- a/litellm/llms/bedrock/chat/converse_transformation.py +++ b/litellm/llms/bedrock/chat/converse_transformation.py @@ -3093,12 +3093,13 @@ class AmazonConverseConfig(BaseConfig): ## HANDLE TOOL CALLS _message: Final = Message(**chat_completion_message) - mapped_finish_reason: Final = map_finish_reason(completion_response["stopReason"]) + raw_finish_reason: Final = completion_response["stopReason"] + mapped_finish_reason: Final = map_finish_reason(raw_finish_reason) initial_finish_reason: Final = ( "stop" if resolved_json_mode and not filtered_tools and tools and mapped_finish_reason == "tool_calls" - else completion_response["stopReason"] + else raw_finish_reason ) ( @@ -3109,11 +3110,17 @@ class AmazonConverseConfig(BaseConfig): tools=optional_params.get("tools"), initial_finish_reason=initial_finish_reason, ) + choice_provider_specific_fields: Final = ( + {"native_finish_reason": raw_finish_reason} # mutable-ok: Choices requires a dict + if returned_finish_reason != raw_finish_reason + else None + ) model_response.choices = [ litellm.Choices( finish_reason=returned_finish_reason, index=0, message=returned_message, + provider_specific_fields=choice_provider_specific_fields, ) ] model_response.created = int(time.time()) diff --git a/tests/unit/llms/bedrock/chat/test_converse_transformation.py b/tests/unit/llms/bedrock/chat/test_converse_transformation.py index 5a4684ce1fb..2cb3d7ac604 100644 --- a/tests/unit/llms/bedrock/chat/test_converse_transformation.py +++ b/tests/unit/llms/bedrock/chat/test_converse_transformation.py @@ -6010,6 +6010,69 @@ def test_transform_response_preserves_raw_bedrock_stop_reason(stop_reason: str): } +def test_transform_response_preserves_raw_stop_reason_when_content_becomes_tool_call(): + response_json = { + "metrics": {"latencyMs": 1}, + "output": { + "message": { + "role": "assistant", + "content": [ + { + "text": json.dumps( + { + "type": "function", + "name": "lookup_weather", + "parameters": {"city": "Seoul"}, + } + ) + } + ], + } + }, + "stopReason": "stop_sequence", + "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={ + "tools": [ + { + "type": "function", + "function": { + "name": "lookup_weather", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}}, + }, + }, + } + ] + }, + litellm_params={}, + encoding=None, + ) + + assert result.choices[0].finish_reason == "tool_calls" + assert result.choices[0].provider_specific_fields == { + "native_finish_reason": "stop_sequence" + } + + # AWS Bedrock Converse stopReason values, accessed 2026-09-16: # https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_Converse.html @pytest.mark.parametrize("stop_reason", ("stop_sequence", "end_turn"))