Skip to content

reask_anthropic_tools retry fails with HTTP 400 on AWS Bedrock — ToolUseBlock.caller=None serialized as null in retry message #2277

@adgapar

Description

@adgapar
  • This is actually a bug report.
  • I am not getting good LLM Results
  • [] I have tried asking for help in the community on discord or discussions and have not received a response.
  • [] I have tried searching the documentation and have not found an answer.

What Model are you using?

  • gpt-3.5-turbo
  • gpt-4-turbo
  • gpt-4
  • Other (please specify): claude-sonnet-4-6 via AWS Bedrock (anthropic.AnthropicBedrock)

Describe the bug

When a Pydantic validation error occurs during a structured extraction call using ANTHROPIC_TOOLS or ANTHROPIC_REASONING_TOOLS mode via AWS Bedrock, the retry mechanism (reask_anthropic_tools) fails on every subsequent attempt with HTTP 400. The error is:

Error code: 400 - {'message': 'messages.1.content.1.tool_use.caller: Input should be a valid dictionary or object to extract fields from'}

Root cause: anthropic==0.88.0 added a new caller: Optional[Caller] = None field to ToolUseBlock. Bedrock does not populate this field — it returns caller=None. In reask_anthropic_tools, the failed completion is serialized via plain model_dump(), which produces "caller": null in the assistant message. The Anthropic API rejects null for this field.

# providers/anthropic/utils.py — reask_anthropic_tools, line 168
for content in response.content:
    assistant_content.append(content.model_dump())  # ← includes "caller": null for Bedrock responses

Why it only affects Bedrock: The direct Anthropic API correctly populates caller=DirectCaller(type='direct') on tool use responses, so model_dump() produces a valid dict. Bedrock returns caller=None, so model_dump() produces "caller": null, which the API rejects.

Verified with live calls to both providers using identical code and the same impossible validation constraint (value: int = Field(ge=10, le=5)) to force retries:

# Direct Anthropic API — generation 2 response:
ToolUseBlock(caller=DirectCaller(type='direct'), ...)
# model_dump() → {"caller": {"type": "direct"}, ...}  ✓ retries succeed (Pydantic errors only)

# Bedrock — generation 2 response:
ToolUseBlock(caller=None, ...)
# model_dump() → {"caller": null, ...}  ✗ generation 3 fails with HTTP 400

On direct Anthropic API, all 3 generations retry successfully and only fail due to the impossible Pydantic constraint — proving the retry mechanism itself works when caller is populated. On Bedrock, generation 3 immediately returns 400.

Both ANTHROPIC_TOOLS and ANTHROPIC_REASONING_TOOLS are affected since they share the same reask_anthropic_tools handler (see ANTHROPIC_HANDLERS registry in the same file).


To Reproduce

import instructor
import anthropic
from pydantic import BaseModel, Field

class MyExtraction(BaseModel):
    summary: str
    value: int = Field(ge=10, le=5)  # impossible constraint — forces validation failure on every generation

bedrock_client = instructor.from_anthropic(
    anthropic.AnthropicBedrock(
        aws_access_key="...",
        aws_secret_key="...",
        aws_region="us-east-1",
    ),
    mode=instructor.Mode.ANTHROPIC_REASONING_TOOLS,
)

try:
    response = bedrock_client.create(
        model="us.anthropic.claude-sonnet-4-6",
        max_tokens=4000,
        thinking={"type": "enabled", "budget_tokens": 2000},
        messages=[{"role": "user", "content": "Extract: some text"}],
        response_model=MyExtraction,
    )
except Exception as e:
    print(type(e).__name__, str(e))

What happens step by step:

  1. Generation 1: Model notices the contradictory schema and refuses to call the tool → list should have at least 1 item validation error (no ToolUseBlock yet).
  2. Generation 2: After reask, model calls the tool with value=10 → Pydantic ge=10, le=5 validation fails. Bedrock returns ToolUseBlock(caller=None, ...).
  3. reask_anthropic_tools serializes the generation 2 response via content.model_dump(), producing {"caller": null, ...} in the assistant message.
  4. Generation 3: API immediately returns HTTP 400 — all further retries fail identically.

Actual reproduction output (confirmed on AWS Bedrock, eu-west-1):

Generation 2 completion showing caller=None:

ToolUseBlock(
    id='toolu_bdrk_015F3ce6KTnAWmxzcMFoGtdz',
    caller=None,   # ← Bedrock does not populate this field
    input={'summary': 'some text', 'value': 10},
    name='MyExtraction',
    type='tool_use'
)

Generation 3 exception:

Error code: 400 - {'message': 'messages.3.content.1.tool_use.caller: Input should be a valid dictionary or object to extract fields from'}

Expected behavior

After a Pydantic validation error, the retry should succeed. The assistant message sent back to the API should not include "caller": null. The caller field should be omitted when it is None so the API accepts the retry message.


Screenshots

N/A — error is reproduced programmatically.


Proposed fix

In providers/anthropic/utils.py, reask_anthropic_tools, line 168 — use exclude_none=True:

# Before:
assistant_content.append(content.model_dump())

# After:
assistant_content.append(content.model_dump(exclude_none=True))

This strips "caller": null from Bedrock responses while leaving direct Anthropic API responses unchanged (where caller is a valid DirectCaller dict and would be preserved).

Environment:

  • instructor==1.15.1
  • anthropic==0.88.0
  • Python 3.13
  • AWS Bedrock (AnthropicBedrock)

Metadata

Metadata

Assignees

No one assigned

    Labels

    anthropicbugSomething isn't workingpythonPull requests that update python code

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions