fix(anthropic): support Bedrock token counting - #5480
Conversation
Move the low-level Bedrock CountTokens logic out of AnthropicModel into models/_anthropic_bedrock_count_tokens.py, document why the SDK's high-level method can't be used, and replace the mock-only test with a recorded VCR test against real Bedrock (claude-sonnet-4 base model id).
…pic-bedrock-count-tokens # Conflicts: # pydantic_ai_slim/pydantic_ai/models/anthropic.py
|
David's AICA here: thanks @dyscott — your approach is correct, and we verified it works against real Bedrock: Cleanup we layered on top of your commits (authorship preserved):
The approach itself is unchanged — purely a tidy-up plus the recorded test. Thanks again for the contribution! |
📝 WalkthroughWalkthroughThe PR adds a Bedrock-specific token counting helper that posts to 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ 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 |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/models/test_anthropic.py`:
- Around line 10451-10455: The AsyncAnthropicBedrock instantiation contains a
hardcoded secret-like literal for the aws_secret_key parameter, which triggers
Ruff S106. To fix this, either replace the hardcoded 'test-secret-key' value
with an environment variable lookup (using a pattern similar to the VCR test in
the file), or add a scoped # noqa: S106 comment on the line if the hardcoded
test value is intentional. Choose the approach that aligns with how other test
cases in this file handle sensitive test credentials.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: afe4ee65-5654-4ad6-9ff4-34ac96d6f691
📒 Files selected for processing (5)
docs/models/anthropic.mdpydantic_ai_slim/pydantic_ai/models/_anthropic_bedrock_count_tokens.pypydantic_ai_slim/pydantic_ai/models/anthropic.pytests/models/cassettes/test_anthropic/test_anthropic_bedrock_count_tokens_real_api.yamltests/models/test_anthropic.py
…n botocore - Use cast_to=object instead of dict[str, object]: the anthropic SDK passes cast_to to issubclass(), which raises TypeError on a subscripted generic under Python 3.10; validate the raw dict explicitly instead. - Skip the real-API VCR test when botocore is absent (AsyncAnthropicBedrock's SigV4 signer imports it at request time; not in the default install).
- Add `test_anthropic_bedrock_count_tokens_error`: a CRIS inference-profile id now surfaces Bedrock's own 400 as `ModelHTTPError` (the headline behavior), recorded live against Bedrock CountTokens. - Add `test_anthropic_bedrock_count_tokens_before_request`: exercises the user-facing `agent.run(count_tokens_before_request=True)` path, reusing the existing `_real_api` cassette. - Document why `test_anthropic_bedrock_count_tokens_unexpected_response` mocks the response instead of using a cassette. - Drop `test_anthropic_process_response_server_tool_blocks`: out-of-scope, tested pre-existing untouched code via a private method.
|
David's AICA here: 🔍 Local review suite (automated, pre-merge)
Verdict: ✅ all 3 waves passed, 0 blocking, 0 required Two earlier informational findings have now been addressed:
|
…ool rejection - Move the four Bedrock count_tokens tests (and their cassettes) out of the test_anthropic.py monolith into a dedicated test_anthropic_bedrock_count_tokens.py feature file. - Add `test_anthropic_bedrock_count_tokens_rejects_server_tools`: confirms (against a real recorded Bedrock response) that the InvokeModel /count-tokens endpoint 400s on a `web_search` server tool, exactly like the standard endpoint — so the strip-to-MemoryTool is necessary on Bedrock too and there is no Bedrock-specific undercounting.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/models/test_anthropic_bedrock_count_tokens.py`:
- Around line 59-63: The test setup in AsyncAnthropicBedrock uses a literal
secret value that triggers Ruff S106, so update the aws_secret_key assignment in
the test to avoid a hardcoded secret. Either add the inline noqa suppression on
the aws_secret_key line or source the key from os.environ, keeping the change
localized to the AsyncAnthropicBedrock instantiation in the test.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: bdfa7283-a3cc-43d3-be99-65b4fa871d2b
📒 Files selected for processing (5)
tests/models/cassettes/test_anthropic_bedrock_count_tokens/test_anthropic_bedrock_count_tokens_error.yamltests/models/cassettes/test_anthropic_bedrock_count_tokens/test_anthropic_bedrock_count_tokens_real_api.yamltests/models/cassettes/test_anthropic_bedrock_count_tokens/test_anthropic_bedrock_count_tokens_rejects_server_tools.yamltests/models/test_anthropic.pytests/models/test_anthropic_bedrock_count_tokens.py
💤 Files with no reviewable changes (3)
- tests/models/cassettes/test_anthropic_bedrock_count_tokens/test_anthropic_bedrock_count_tokens_real_api.yaml
- tests/models/cassettes/test_anthropic_bedrock_count_tokens/test_anthropic_bedrock_count_tokens_error.yaml
- tests/models/test_anthropic.py
✅ Files skipped from review due to trivial changes (1)
- tests/models/cassettes/test_anthropic_bedrock_count_tokens/test_anthropic_bedrock_count_tokens_rejects_server_tools.yaml
…ts into try_import gate
Co-authored-by: Dylan Scott <dylan@sweetspot.so> Co-authored-by: David Sanchez <64162682+dsfaccini@users.noreply.github.com>
Summary
AsyncAnthropicBedrockguard now that Bedrock supports CountTokens for Claude models.AsyncAnthropicBedrockclient.input.invokeModel.bodyCountTokens shape, base64-encode the inner body for the raw HTTP JSON protocol, and returnBetaMessageTokensCount(input_tokens=...).post(...)method directly. I am not sure if what I implemented would be the best pattern going forward, so looking to hear feedback on this.Testing Plan
tests/models/test_anthropic.pycover the Bedrock CountTokens request shape, including the base64-encodedinput.invokeModel.body,system,anthropic_betas,extra_body, and timeout handling.anthropic.claude-haiku-4-5-20251001-v1:0on Bedrock and got a successful token count response.uv run pytest tests/models/test_anthropic.py::test_anthropic_bedrock_count_tokens tests/models/ test_anthropic.py::test_anthropic_bedrock_count_tokens_unexpected_response tests/models/ test_anthropic.py::test_anthropic_process_response_server_tool_blocks tests/models/ test_anthropic.py::test_anthropic_count_tokens_with_no_messagesuv run ruff check pydantic_ai_slim/pydantic_ai/models/anthropic.py tests/models/test_anthropic.pyuv run ruff format --check pydantic_ai_slim/pydantic_ai/models/anthropic.py tests/models/test_anthropic.pyuv run pyright pydantic_ai_slim/pydantic_ai/models/anthropic.py tests/models/test_anthropic.pyChecklist