fix(bedrock): preserve native converse stop reason

This commit is contained in:
david-koh 2026-09-16 16:25:32 +09:00
parent d947ec1f7d
commit ba63fe7435
3 changed files with 65 additions and 3 deletions

View file

@ -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"]
)
(

View file

@ -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,

View file

@ -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 = {