fix(bedrock): preserve native reason through conversions

This commit is contained in:
david-koh 2026-09-28 14:05:21 +09:00
parent 18c6391ea4
commit d54c0d4976
3 changed files with 83 additions and 10 deletions

View file

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

View file

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

View file

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