fix(llmobs): capture Anthropic tool definitions, calls, and results on Bedrock InvokeModel - #19944
Conversation
…edrock InvokeModel The Bedrock InvokeModel integration read only the first content block of an Anthropic response, and only its `text` field. Claude's Messages API returns a list of tagged blocks, so blocks after the first were discarded and a `tool_use` block produced an empty string. Adds `get_messages_from_anthropic_content()` to the shared integration utils and uses it for the Anthropic branch of `_extract_output_message`. Tool calls are now captured on InvokeModel spans, matching Converse, and responses that begin with a `tool_use` or `thinking` block keep their assistant text. Output messages for Anthropic InvokeModel now carry `role: "assistant"`, matching the Anthropic and Converse integrations.
…e SDK integration `anthropic.py` had its own copy of the content-block extraction logic, identical in purpose to the one now in `_integrations/utils.py`. Two parsers for the same payload is how the Bedrock InvokeModel path drifted: it was written for the March 2024 single-text-block shape and never updated as Anthropic added `tool_use` and `thinking` blocks, while `anthropic.py` was. Delegates `AnthropicIntegration._extract_output_message` and `_format_tool_result_content` to the shared helpers so there is one implementation to maintain. No behavior change. This commit is separable: dropping it leaves the Bedrock fix intact, at the cost of keeping the duplicate parser in `anthropic.py`.
…d output paths `ToolCall` and `ToolResult` construction from Anthropic content blocks was written out separately in the input and output extraction paths, including the JSON-string normalization of `tool_use.input`. Extracts `anthropic_tool_call_from_block()` and `anthropic_tool_result_from_block()` so both paths build them the same way, and removes the now-redundant `_format_tool_result_content` pass-through. Also restores the stricter `list` guard on the shared content extractor. The earlier `Iterable` check accepted dicts, which would have iterated keys instead of returning no messages. No behavior change.
…ut messages `_extract_input_message` handled only `text` and `image` blocks, with no fallback, so `tool_use`, `tool_result`, and `thinking` blocks in Anthropic input messages were dropped entirely. Agent loops replay the assistant's tool call and the user's tool result as conversation history, so the span input showed only the text turns and the trace read as if the model had answered without using a tool. Handles those block types using the shared Anthropic builders, matching the Anthropic SDK integration.
…ropic models Tool definitions were only read from the Converse `toolConfig` field, so spans for `InvokeModel` calls never reported them. Anthropic models carry tool definitions in the request body under `tools`, in Anthropic's own format rather than Converse's `toolSpec` wrapper. Passes `tools` through in the invoke request params and reuses the Anthropic definition extractor, which moves from the Anthropic integration into the shared integration utils so both callers build definitions the same way.
🎉 All green!🧪 All tests passed 🔗 Commit SHA: 0261b2f | Docs | View more details | Give us feedback! |
BenchmarksBenchmark execution time: 2026-08-28 22:29:58 Comparing candidate commit 0261b2f in PR branch Found 0 performance improvements and 8 performance regressions! Performance is the same for 578 metrics, 10 unstable metrics, 1 known flaky benchmarks, 17 flaky benchmarks without significant changes.
|
Description
The Bedrock
InvokeModelintegration loses tool information for Anthropic models in three places:textfield. Atool_useblock produces an empty output, and blocks after the first are discarded (so text is lost when a response starts withtool_useorthinking).textandimageblocks are handled, with no fallback.tool_use,tool_result, andthinkingblocks in conversation history are dropped, so agent-loop traces read as if the model answered without using a tool.toolConfigfield, soInvokeModelnever reports them.Converse/ConverseStreamalready handle all of this, so behavior differed by API for the same model.The cause is duplication: the same Anthropic payload was parsed by three separate copies of the logic, and each new Anthropic block type (
tool_use,thinking) only ever got added to the Anthropic SDK integration. This consolidates that parsing into_integrations/utils.py— one content-block extractor, oneToolCall/ToolResultbuilder pair, one tool-definition extractor — shared by the Anthropic and Bedrock integrations, then uses it on both the request and response paths ofInvokeModel.Commits are independently revertible in order, if reviewers want narrower scope:
fix— output: tool calls and all content blocksrefactor— share the output extractor withanthropic.pyrefactor— share tool block parsing between input and outputfix— input: capture tool blocksfix— capture tool definitionsTesting
24 new unit tests in
tests/llmobs/test_integrations_utils.pycovering each block type, parallel tool calls, JSON-string tool arguments, replayed agent-loop history, and the non-Anthropic response shapes that share the same entry point.Verified against live Bedrock with a two-turn agent loop on Claude Haiku 4.5 — tool definitions, tool call, and tool result all captured with matching
tool_idacross turns.llmobs::anthropic108 passed / 0 failed andcontrib::botocore185 passed / 0 failed.Risks
The refactor moves logic out of a working integration (
anthropic.py) to fix a different one. The existing Anthropic suite is unchanged and passing, which is the check on that.Output messages for Anthropic
InvokeModelnow carryrole: "assistant", matching the Anthropic SDK and Converse integrations. One assertion intest_bedrock_llmobs.pywas updated for this.Additional Notes
Commit 5 adds one line to
contrib/internal/botocore/services/bedrock.pyto passtoolsthrough in the invoke request params — the LLMObs layer has no other access to the request body.Not included:
InvokeModelWithResponseStreamdrops tool arguments in a separate function, andmax_tokensmetadata still reads the legacymax_tokens_to_samplefield.