feat(aws): native async for ChatBedrockConverse, ChatBedrock and BedrockEmbeddings - #1256
Open
roi hezkiyahu (roihezkiRaven) wants to merge 5 commits into
Open
Conversation
`boto3` is synchronous, so every async method in this package bridges to it with `run_in_executor`. That holds a thread for the whole duration of a model call, capping concurrency at the default executor size, `min(32, cpu + 4)`. `aiobotocore` is the obvious fix but cannot be depended on here: it pins a narrow, trailing `botocore` window (3.9.0 allows `<1.43.57`) while this package requires `boto3>=1.43.64`, and that floor moves forward with each new AWS API. The ranges cross permanently, so neither `aioboto3` nor `aiobotocore` resolves against langchain-aws today. `BedrockAsyncClient` instead signs with `botocore` and sends with `httpx` — the approach the Anthropic SDK uses for Bedrock, and the one `ChatAnthropicBedrock` already benefits from for Anthropic models only. It has no version conflict, and `httpx` is already in the dependency tree via `langchain-core`. Event stream framing is parsed by `botocore`'s own `EventStreamBuffer` rather than reimplemented, and header-bound request members are mapped explicitly so that guardrails cannot be silently dropped. Transient errors are retried with jittered backoff, since boto3 retries by default and losing that would be a regression against the sync path. Method names and payload shapes match the `boto3` client, so an `aiobotocore` client remains usable wherever a caller prefers one. `AsyncTransportMixin` holds the setup, access and teardown shared by the models that use it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`_generate` and `_stream` duplicated their entire request-building block, so that is extracted first; `_agenerate` and `_astream` then reuse it, keeping the sync and async paths from drifting apart. Both fall back to the previous executor behavior when no async client is configured, so existing callers are unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`aembed_query` and `aembed_documents` ran the sync methods in an executor. Splitting `_embedding_func` into request building and response parsing lets the async path share the per-provider logic, and the Cohere multi-input batches now fan out concurrently rather than serially. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`_aprepare_input_and_invoke_stream` already existed but obtained its response through `run_in_executor`, so the Invoke API had the same ceiling as Converse. It now uses the async client when one is configured, and `_aprepare_input_and_invoke` is added for the non-streaming path. `aprepare_output_stream` iterated the response body synchronously, which only works for botocore's `EventStream`; it now accepts either kind of stream. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Unit tests drive the transport through a fake HTTP client with real event stream frames, and the models through async client doubles; no network, so they run under `--disable-socket` like the rest of the suite. Each model has a test asserting the sync and async paths send byte-identical requests, which is what stops the two from drifting apart. Integration tests cover the same ground against real Bedrock using the cheapest available models with one-token prompts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
roi hezkiyahu (roihezkiRaven)
force-pushed
the
feat/aiobotocore-async-bedrock
branch
from
September 2, 2026 20:00
b879eeb to
4f84ed2
Compare
roi hezkiyahu (roihezkiRaven)
marked this pull request as ready for review
September 2, 2026 20:01
Author
|
Hi Michael Chin (@michaelnchin), is this something that we can push somehow? Would love to get this out with your help :) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #663.
Why
ainvoke/astream/aembed_*bridge to the synchronousboto3client withrun_in_executor. The thread is held for the whole model call, so concurrency is capped at the default executor size,min(32, os.cpu_count() + 4)— the ceiling Lucas Kim (@kimnamu) measured in #663, and the cause of the latency growth Norman Luengo (@norman-luengo) reported.Against a local fake
bedrock-runtimewith 500 ms of injected latency, on a 12-core box (16-thread default executor):ainvokeBelow the ceiling there is no gain, and slightly more overhead; above it the executor serializes into waves while the native path stays flat. A fake endpoint is used deliberately: against real Bedrock the wall clock is dominated by per-account service quotas, so it measures the account rather than the client.
Try it
Integration tests covering the same ground live in
tests/integration_tests/test_async_client.py; they use Nova Micro and Titan v2 with one-token prompts, so a full run costs a fraction of a cent.Why not aiobotocore
The issue asks for
aiobotocore/aioboto3support, but neither can be a dependency of this package:aiobotocore3.9.0 (latest) pinsbotocore>=1.43.3,<1.43.57; this package requiresboto3>=1.43.64.pip install langchain-aws aiobotocorehas no solution today.aiobotocore's ceiling trails botocore by design, while this package's floor moves forward for new AWS APIs (1.42.42 → 1.43.32 → 1.43.64, the last in feat(aws): add DynamoDBVectorStore backed by DynamoDB vector indexes #1200 for DynamoDB vector indexes). The ranges cross and stay crossed.aioboto3is further behind: its latest release pinsaiobotocore==2.25.1, i.e.botocore<1.40.62.So
BedrockAsyncClientsigns withbotocoreand sends withhttpx— the approach Anthropic's SDK takes, and the oneChatAnthropicBedrockalready benefits from for Anthropic models only. This covers the rest of the surface (Nova, Titan, Cohere, Llama, embeddings). Event stream framing is parsed by botocore's ownEventStreamBufferrather than reimplemented.No new dependencies are declared —
httpxis already in the tree vialangchain-core.async_clientis duck-typed on theboto3method names, so anyone who prefersaiobotocorecan pass their own entered client and manage itsbotocorepin themselves.Reading it
Five commits, in dependency order:
feat: add a native async Bedrock runtime client— the newasync_client.pyand_async_transport.py, plus a factory inutils.py. Purely additive (+919/−3); nothing else in the package changes behavior.feat: ... ChatBedrockConverse— the bulk of the diff is an extraction:_generateand_streamduplicated ~50 lines of request building verbatim, now shared._agenerate/_astreamreuse it.feat: ... BedrockEmbeddings— same shape:_embedding_funcsplit into request building and response parsing so both paths share the per-provider logic.feat: ... ChatBedrock and BedrockBase— same again for the Invoke API.test: cover the native async Bedrock paths— 52 unit tests and 9 integration tests.Commits 2–4 are refactor-then-reuse. The refactors are meant to be behavior-preserving, and the check for that is mechanical: all 1307 pre-existing unit tests pass untouched, and each model has a test asserting the sync and async paths send byte-identical requests, so the two cannot drift apart.
Opt-in and additive throughout: with neither
use_async_transportnorasync_clientset, every async method keeps its current executor behavior.Decisions worth a second opinion
retries["max_attempts"]. This is not botocore's adaptive mode and shares no retry quota;retries["mode"]is ignored and documented as such. I included retries because boto3 retries by default, so shipping none would silently regress the async path against the sync one. Happy to drop them if you would rather retries stayed with the caller.httpxpool binds to the loop that first drives it. Sharing one client across loops previously hung forever with nothing raised (reproduced: 25 threads each runningasyncio.runon one shared model — 14 of 75 calls completed, the rest deadlocked inselectors.select). It now raises immediately with an actionable message. Louder than before, and deliberate, but it is a behavior choice.use_async_transport=Truetogether with abedrock_api_keyraises rather than silently authenticating as a different identity. Same approach as fix(aws): Raise error on unsupported guardrail header usage with Mantle #1225.Known deviations from boto3
usageis a superset of the sync path's.ReasoningContentBlock.redactedContentstays a base64strrather thanbytes. It is passed through opaquely so it round-trips, but it differs from the sync path._INVOKE_HEADER_MEMBERS. If that mapping drifts upstream, guardrails would quietly stop being applied — worth a check by someone who knows the model well.Testing done
--disable-socket;make lintclean, andmypyclean under each of 3.10 / 3.11 / 3.12 / 3.13.InvokeModelWithResponseStreammovesaccepttoX-Amzn-Bedrock-Accept.Disclaimer: this contribution was developed with the assistance of AI agents (Claude), including adversarial review passes that found several defects in my own first draft (silently dropped guardrail headers, swallowed event-stream error frames, and a cross-loop deadlock). The analysis, benchmarks and code were produced under my direction, and I have reviewed the result.