fix(bedrock): map the full Converse stopReason set to finish_reason - #1358
Conversation
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Team Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review. WalkthroughBedrock stop reasons now use a shared mapping for streaming and non-streaming responses. Tests cover service-model reasons, missing stop reasons, and guardrail-blocked structured output. ChangesBedrock stop-reason mapping
Suggested reviewers: Merge Risk: ⚪ Minimal · up to Bedrock responses now report tool calls, token limits, content filtering, and guardrail intervention through the appropriate finish reasons in both streaming and non-streaming flows. The implementation has focused coverage for these behaviors, with no remaining current-head merge risk identified. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@emecii please check the conflicts. Otherwise LGTM. |
Codecov Report✅ All modified and coverable lines are covered by tests.
... and 1 file with indirect coverage changes 🚀 New features to boost your workflow:
|
9e976ca to
229b5db
Compare
229b5db to
05612c7
Compare
The Converse API reports nine stop reasons but the provider mapped three,
sending everything else to "stop". A guardrail block
("guardrail_intervened"), a content filter hit ("content_filtered") and a
context overflow ("model_context_window_exceeded") therefore looked like a
normal completion.
The visible failure is the structured-output guard in any_llm.py: with
finish_reason="stop" the content_filter branch never fires, so the
guardrail's blocked-message prose reaches parse_json_content() and the
caller gets a pydantic ValidationError instead of
ContentFilterFinishReasonError.
Add BEDROCK_STOP_REASON_TO_FINISH_REASON, mirroring
ANTHROPIC_STOP_REASON_TO_FINISH_REASON, and route both the streaming and
non-streaming paths through it. "stop_sequence", "malformed_model_output"
and "malformed_tool_use" have no OpenAI counterpart and keep falling
through to "stop".
The tests read the stopReason enum from botocore's bedrock-runtime service
model and parametrize over it, so a botocore upgrade that adds a reason
fails the suite rather than silently defaulting it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
05612c7 to
9631447
Compare
Description
The Bedrock provider maps only three of the Converse API's nine
stopReasonvalues, so a guardrail-blocked or context-overflowed response reaches the caller as an ordinaryfinish_reason="stop".src/any_llm/providers/bedrock/utils.py:517(non-streaming):and
:611-617(streaming) handlesmax_tokensandtool_useand sends everything else to"stop".The visible failure is the structured-output guard in
src/any_llm/any_llm.py:798-806. When a Bedrock Guardrail blocks a response, Converse returnsstopReason="guardrail_intervened"with the guardrail's blocked-message text as content. Because that arrives asfinish_reason="stop", thecontent_filterbranch never fires and the guardrail's prose is handed toparse_json_content()instead, so the caller gets a pydanticValidationErrorfrom an unrelated layer rather than theContentFilterFinishReasonErrorthis library raises for every other provider.content_filteredbehaves the same way, andmodel_context_window_exceededreports as"stop"rather than"length".This is the same fix already merged for gemini (#1202), zai (#1204), cohere (#1301) and anthropic (#1306/#1328). Bedrock was the last provider still hardcoding a partial mapping, so this follows
ANTHROPIC_STOP_REASON_TO_FINISH_REASONin shape and naming:stop_sequence,malformed_model_outputandmalformed_tool_usehave no OpenAI counterpart and keep falling through to the"stop"default, as do any values a future service model adds. Both call sites now go through one_map_stop_reason()helper, which also lets thecastat the non-streaming call site go away.The nine-value enum is not taken from the AWS docs prose. It is read out of the installed botocore service model (
bedrock-runtime, shapeStopReason), which is the same source the SDK validates against:The tests read the enum from that same service model and parametrize over it, so a botocore upgrade that adds a stop reason fails the suite instead of silently defaulting the new reason to
"stop".Reproduction
Before:
After:
Tests
Added to
tests/unit/providers/test_aws_provider.py:test_convert_response_maps_every_bedrock_stop_reasonandtest_streaming_chunk_maps_every_bedrock_stop_reason, parametrized over the full botocoreStopReasonenum, asserting both paths agree.test_convert_response_without_stop_reason_finishes_as_stop.test_guardrail_blocked_structured_output_raises_content_filter_error, the end-to-end case above.On the unfixed tree these fail:
With the fix,
uv run pytest tests/unit/providers/test_aws_provider.py→ 116 passed.The
[tool_use]case is the one behaviour change beyond the three broken reasons: astopReason="tool_use"response that carries notoolUseblock now reports"tool_calls"instead of"stop". Both realtool_useshapes (a genuine tool call, and the syntheticany_llm_structured_outputunwrap) return earlier in_convert_responseand are untouched.PR Type
Relevant issues
No open issue. Same fix as #1202 / #1204 / #1301 / #1328, applied to the remaining provider.
Checklist
Verification
uv run pre-commit run --all-files(ruff, ruff-format, mypy, codespell) clean.uv run pytest tests/unit/providers/test_aws_provider.py→ 116 passed.uv run pytest tests/unit→ 2308 passed, 69 skipped, 16 failed. All 16 failures are intest_anthropic_messages.py/test_anthropic_provider.pyand reproduce identically on an unmodifiedmainin this environment:TypeError: Invalid 'http_client' argument; Expected an instance of httpx2.AsyncClient but got <class 'httpx.AsyncClient'>, from what a localuv sync --all-extras -Uresolves. Nothing bedrock-related.AI Usage Information
AI Model used: Claude Opus 5
AI Developer Tool used: Claude Code
Any other info you'd like to share: The stop-reason enum was read from the installed botocore service model rather than the AWS docs, and the tests read it from the same place so the list cannot drift.
I am an AI Agent filling out this form (check box if true)
Summary by CodeRabbit
stop.