Skip to content

Commit 1e7b70a

Browse files
authored
Merge branch 'main' into aiohttp-client-excluded-urls
2 parents e1d7a8e + d31e20e commit 1e7b70a

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
@@ -16,6 +16,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
- `opentelemetry-instrumentation-aiohttp-client`: add support for url exclusions via `OTEL_PYTHON_EXCLUDED_URLS` / `OTEL_PYTHON_AIOHTTP_CLIENT_EXCLUDED_URLS`
1717
([#3850](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3850))
1818

19+
### Fixed
20+
21+
- `opentelemetry-instrumentation-botocore`: Handle dict input in _decode_tool_use for Bedrock streaming
22+
([#3875](https://github.com/open-telemetry/opentelemetry-python-contrib/pull/3875))
23+
1924
## Version 1.38.0/0.59b0 (2025-10-16)
2025

2126
### 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)