Skip to content

Commit d31e20e

Browse files
authored
Only decode JSON input buffer in Anthropic Claude streaming (#3875)
* Only decode JSON input buffer in Anthropic Claude streaming _decode_tool_use was only used when _tool_json_input_buf was found, but we were decoding the entire _content_block after adding _tool_json_input_buf to it. The _content_block overall which could contain non-JSON elements (e.g. {}), causing failures. To fix this, we have removed _decode_tool_use helper function and inlined JSON decoding logic directly into content_block_stop handler in _process_anthropic_claude_chunk, where we only use it to decode _tool_json_input_buf before appending to _content_block. * Update test_botocore_bedrock.py Fix lint: `opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py:2990:4: C0415: Import outside toplevel (opentelemetry.instrumentation.botocore.extensions.bedrock_utils.InvokeModelWithResponseStreamWrapper) (import-outside-toplevel)` * Update test_botocore_bedrock.py Remove extra line * fix lint issue
1 parent 34db73e commit d31e20e

File tree

3 files changed

+88
-14
lines changed

3 files changed

+88
-14
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
1212
## Unreleased
1313

14+
### Fixed
15+
16+
- `opentelemetry-instrumentation-botocore`: Handle dict input in _decode_tool_use for Bedrock streaming
17+
([#3875](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3875))
18+
1419
## Version 1.38.0/0.59b0 (2025-10-16)
1520

1621
### Fixed

instrumentation/opentelemetry-instrumentation-botocore/src/opentelemetry/instrumentation/botocore/extensions/bedrock_utils.py

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,6 @@
3636
_StreamErrorCallableT = Callable[[Exception], None]
3737

3838

39-
def _decode_tool_use(tool_use):
40-
# input get sent encoded in json
41-
if "input" in tool_use:
42-
try:
43-
tool_use["input"] = json.loads(tool_use["input"])
44-
except json.JSONDecodeError:
45-
pass
46-
return tool_use
47-
48-
4939
# pylint: disable=abstract-method
5040
class ConverseStreamWrapper(ObjectProxy):
5141
"""Wrapper for botocore.eventstream.EventStream"""
@@ -368,10 +358,13 @@ def _process_anthropic_claude_chunk(self, chunk):
368358
if message_type == "content_block_stop":
369359
# {'type': 'content_block_stop', 'index': 0}
370360
if self._tool_json_input_buf:
371-
self._content_block["input"] = self._tool_json_input_buf
372-
self._message["content"].append(
373-
_decode_tool_use(self._content_block)
374-
)
361+
try:
362+
self._content_block["input"] = json.loads(
363+
self._tool_json_input_buf
364+
)
365+
except json.JSONDecodeError:
366+
self._content_block["input"] = self._tool_json_input_buf
367+
self._message["content"].append(self._content_block)
375368
self._content_block = {}
376369
self._tool_json_input_buf = ""
377370
return

instrumentation/opentelemetry-instrumentation-botocore/tests/test_botocore_bedrock.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
from botocore.eventstream import EventStream, EventStreamError
2626
from botocore.response import StreamingBody
2727

28+
from opentelemetry.instrumentation.botocore.extensions.bedrock_utils import (
29+
InvokeModelWithResponseStreamWrapper,
30+
)
2831
from opentelemetry.semconv._incubating.attributes.error_attributes import (
2932
ERROR_TYPE,
3033
)
@@ -2975,6 +2978,79 @@ def test_invoke_model_with_response_stream_invalid_model(
29752978
assert len(logs) == 0
29762979

29772980

2981+
@pytest.mark.parametrize(
2982+
"input_value,expected_output",
2983+
[
2984+
({"location": "Seattle"}, {"location": "Seattle"}),
2985+
({}, {}),
2986+
(None, None),
2987+
],
2988+
)
2989+
def test_anthropic_claude_chunk_tool_use_input_handling(
2990+
input_value, expected_output
2991+
):
2992+
"""Test that _process_anthropic_claude_chunk handles various tool_use input formats."""
2993+
2994+
def stream_done_callback(response, ended):
2995+
pass
2996+
2997+
def stream_error_callback(exc, ended):
2998+
pass
2999+
3000+
wrapper = InvokeModelWithResponseStreamWrapper(
3001+
stream=mock.MagicMock(),
3002+
stream_done_callback=stream_done_callback,
3003+
stream_error_callback=stream_error_callback,
3004+
model_id="anthropic.claude-3-5-sonnet-20240620-v1:0",
3005+
)
3006+
3007+
# Simulate message_start
3008+
wrapper._process_anthropic_claude_chunk(
3009+
{
3010+
"type": "message_start",
3011+
"message": {
3012+
"role": "assistant",
3013+
"content": [],
3014+
},
3015+
}
3016+
)
3017+
3018+
# Simulate content_block_start with specified input
3019+
content_block = {
3020+
"type": "tool_use",
3021+
"id": "test_id",
3022+
"name": "test_tool",
3023+
}
3024+
if input_value is not None:
3025+
content_block["input"] = input_value
3026+
3027+
wrapper._process_anthropic_claude_chunk(
3028+
{
3029+
"type": "content_block_start",
3030+
"index": 0,
3031+
"content_block": content_block,
3032+
}
3033+
)
3034+
3035+
# Simulate content_block_stop
3036+
wrapper._process_anthropic_claude_chunk(
3037+
{"type": "content_block_stop", "index": 0}
3038+
)
3039+
3040+
# Verify the message content
3041+
assert len(wrapper._message["content"]) == 1
3042+
tool_block = wrapper._message["content"][0]
3043+
assert tool_block["type"] == "tool_use"
3044+
assert tool_block["id"] == "test_id"
3045+
assert tool_block["name"] == "test_tool"
3046+
3047+
if expected_output is not None:
3048+
assert tool_block["input"] == expected_output
3049+
assert isinstance(tool_block["input"], dict)
3050+
else:
3051+
assert "input" not in tool_block
3052+
3053+
29783054
def amazon_nova_messages():
29793055
return [
29803056
{"role": "user", "content": [{"text": "Say this is a test"}]},

0 commit comments

Comments
 (0)