Skip to content

fix(bedrock): strip "caller: null" from tool_use blocks to prevent Bedrock HTTP 400#1566

Open
xodn348 wants to merge 1 commit into
anthropics:mainfrom
xodn348:fix/bedrock-strip-null-caller-in-tool-use
Open

fix(bedrock): strip "caller: null" from tool_use blocks to prevent Bedrock HTTP 400#1566
xodn348 wants to merge 1 commit into
anthropics:mainfrom
xodn348:fix/bedrock-strip-null-caller-in-tool-use

Conversation

@xodn348
Copy link
Copy Markdown
Contributor

@xodn348 xodn348 commented May 19, 2026

Summary

When a Bedrock streaming or non-streaming response includes a tool_use block where the caller field is None (the common case for model-invoked tools with no explicit caller), calling .to_dict() on the resulting ToolUseBlock object produces {"caller": null, ...}. When this dict is then passed back as part of a follow-up messages.create request, Bedrock returns HTTP 400 with "messages.<n>.content.<m>.tool_use.caller: Input should be a valid dictionary or object to extract fields from" and the conversation loop breaks.

The root cause is a type asymmetry between the response model (ToolUseBlock.caller: Optional[Caller] = None) and the request param type (ToolUseBlockParam.caller: Caller with total=False): the response type allows None, but the request type requires the key to be absent (not null) when there is no caller. The standard Anthropic and Vertex APIs are lenient about caller: null, but Bedrock validates the field strictly.

The fix adds _strip_null_caller_from_tool_use() — called inside _prepare_options() in the Bedrock client — to remove the caller key from any tool_use / server_tool_use content block where it is None before the payload is serialised and sent. The logic is intentionally scoped to the Bedrock code path because only Bedrock enforces this constraint.

Issue

Fixes #1454

Local verification

$ python -m pytest tests/lib/test_bedrock.py -v --override-ini="addopts="
============================= test session starts ==============================
platform linux -- Python 3.11.15, pytest-9.0.2, pluggy-1.6.0 -- /usr/local/bin/python
cachedir: .pytest_cache
rootdir: /tmp/anthropic-sdk-python
configfile: pyproject.toml
plugins: respx-0.22.0, anyio-4.12.1, time-machine-3.2.0, xdist-3.8.0, asyncio-1.3.0, inline-snapshot-0.32.5, http-snapshot-0.1.8
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=session, asyncio_default_test_loop_scope=function
collecting ... collected 14 items

tests/lib/test_bedrock.py::test_messages_retries PASSED                  [  7%]
tests/lib/test_bedrock.py::test_messages_retries_async PASSED            [ 14%]
tests/lib/test_bedrock.py::test_application_inference_profile PASSED     [ 21%]
tests/lib/test_bedrock.py::test_api_key_auth PASSED                      [ 28%]
tests/lib/test_bedrock.py::test_api_key_auth_async PASSED                [ 35%]
tests/lib/test_bedrock.py::test_api_key_from_env PASSED                  [ 42%]
tests/lib/test_bedrock.py::test_api_key_mutual_exclusion PASSED          [ 50%]
tests/lib/test_bedrock.py::test_api_key_mutual_exclusion_async PASSED    [ 57%]
tests/lib/test_bedrock.py::test_api_key_env_mutual_exclusion PASSED      [ 64%]
tests/lib/test_bedrock.py::test_region_infer_from_profile PASSED         [ 71%]
tests/lib/test_bedrock.py::test_region_infer_from_specified_profile[default profile] PASSED [ 78%]
tests/lib/test_bedrock.py::test_region_infer_from_specified_profile[custom profile] PASSED [ 85%]
tests/lib/test_bedrock.py::test_tool_use_null_caller_stripped PASSED     [ 92%]
tests/lib/test_bedrock.py::test_tool_use_null_caller_stripped_async PASSED [100%]

============================== 14 passed in 0.48s ==============================

$ python -m ruff check src/anthropic/lib/bedrock/_client.py tests/lib/test_bedrock.py
All checks passed!

$ python -m mypy src/anthropic/lib/bedrock/_client.py
Success: no issues found in 1 source file
=== LOCAL_TEST_PASSED ===

Risk

The change only affects the Bedrock code path (_prepare_options in src/anthropic/lib/bedrock/_client.py). It removes caller: null from tool_use and server_tool_use content blocks — a value that Bedrock already rejects with a 400, so no previously-working request can break. Users who explicitly set caller to a non-None Caller object are unaffected; only None values are dropped.

…o Bedrock

Bedrock rejects HTTP 400 when a tool_use or server_tool_use content block
contains "caller": null.  This happens when callers convert a response
ToolUseBlock (where caller is Optional[Caller] = None) to a dict via
.to_dict() without exclude_none=True and then pass that dict back as part
of a follow-up messages request.

Add _strip_null_caller_from_tool_use() in the Bedrock client's
_prepare_options() to remove the key before the payload is sent.
Bedrock is stricter than the standard API about this field; the fix is
intentionally scoped to the Bedrock code path.

Fixes anthropics#1454
@xodn348 xodn348 requested a review from a team as a code owner May 19, 2026 09:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ToolUseBlock and ToolUseBlockParam type conflict for caller nullability

1 participant