Skip to content

Commit 462a04b

Browse files
devin-ai-integration[bot]obinnascale3obinnaokaform1kl0shkarthikscale3
authored
Release v3.8.21 - AWS Bedrock ARN parsing fix (#545)
* capture different types of google genai inputs (#534) Co-authored-by: Obinna Okafor <[email protected]> * Read stream before saving attributes. Parse modelId in a more robust way. * Decide based on model name on the set of attributes tracked. * Added basic test for model ID parsing. * Added replay tests for AWS Bedrock. * Added test for streaming requests too. * Obinna/fix awsbedrock streaming response (#538) * fix aws bedrock streaming bug * bump version * add deprecated dependency * restrict current pinecone instrumentation to v6.0.2 * hard code boto3 dependency version * fix duplicate event stream * set test aws key * Fix AWS Bedrock ARN parsing issue in converse methods (#544) * Fix AWS Bedrock ARN parsing issue in converse methods - Replace modelId.split('.') with parse_vendor_and_model_name_from_model_id - Fixes crash when using ARN or cross-region model IDs with multiple dots - Makes patch_converse and patch_converse_stream consistent with other methods Resolves ValueError: too many values to unpack (expected 2) when using: - ARN format: arn:aws:bedrock:us-east-1:<account_id>:inference-profile/us.anthropic.claude-3-haiku-20240307-v1:0 - Cross-region format: us.anthropic.claude-sonnet-4-20250514-v1:0 Co-Authored-By: [email protected] <[email protected]> * Bump version to 3.8.21 for ARN parsing bug fix Co-Authored-By: [email protected] <[email protected]> --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: [email protected] <[email protected]> --------- Co-authored-by: obinnascale3 <[email protected]> Co-authored-by: Obinna Okafor <[email protected]> Co-authored-by: m1kl0sh <[email protected]> Co-authored-by: Obinna Okafor <[email protected]> Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> Co-authored-by: [email protected] <[email protected]>
1 parent b62abd9 commit 462a04b

File tree

10 files changed

+386
-21
lines changed

10 files changed

+386
-21
lines changed

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ dependencies = [
3434
"ujson>=5.10.0",
3535
"boto3==1.38.0",
3636
"setuptools",
37+
"Deprecated==1.2.18",
3738
]
3839

3940
requires-python = ">=3.9"
@@ -47,7 +48,7 @@ dev = [
4748
"qdrant-client",
4849
"graphlit-client",
4950
"python-dotenv",
50-
"pinecone",
51+
"pinecone>=3.1.0,<=6.0.2",
5152
"langchain",
5253
"langchain-community",
5354
"langchain-openai",

src/langtrace_python_sdk/constants/instrumentation/aws_bedrock.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
"METHOD": "aws_bedrock.invoke_model",
66
"ENDPOINT": "/invoke-model",
77
},
8+
"INVOKE_MODEL_WITH_RESPONSE_STREAM": {
9+
"METHOD": "aws_bedrock.invoke_model_with_response_stream",
10+
"ENDPOINT": "/invoke-model-with-response-stream",
11+
},
812
"CONVERSE": {
913
"METHOD": AWSBedrockMethods.CONVERSE.value,
1014
"ENDPOINT": "/converse",

src/langtrace_python_sdk/instrumentation/aws_bedrock/patch.py

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,10 @@
1515
"""
1616

1717
import json
18+
import io
1819

1920
from wrapt import ObjectProxy
21+
from itertools import tee
2022
from .stream_body_wrapper import BufferedStreamBody
2123
from functools import wraps
2224
from langtrace.trace_attributes import (
@@ -43,6 +45,7 @@
4345
set_span_attributes,
4446
set_usage_attributes,
4547
)
48+
from langtrace_python_sdk.utils import set_event_prompt
4649

4750

4851
def converse_stream(original_method, version, tracer):
@@ -104,7 +107,7 @@ def traced_method(wrapped, instance, args, kwargs):
104107
def patch_converse_stream(original_method, tracer, version):
105108
def traced_method(*args, **kwargs):
106109
modelId = kwargs.get("modelId")
107-
(vendor, _) = modelId.split(".")
110+
vendor, _ = parse_vendor_and_model_name_from_model_id(modelId)
108111
input_content = [
109112
{
110113
"role": message.get("role", "user"),
@@ -128,7 +131,9 @@ def traced_method(*args, **kwargs):
128131
response = original_method(*args, **kwargs)
129132

130133
if span.is_recording():
131-
set_span_streaming_response(span, response)
134+
stream1, stream2 = tee(response["stream"])
135+
set_span_streaming_response(span, stream1)
136+
response["stream"] = stream2
132137
return response
133138

134139
return traced_method
@@ -137,7 +142,7 @@ def traced_method(*args, **kwargs):
137142
def patch_converse(original_method, tracer, version):
138143
def traced_method(*args, **kwargs):
139144
modelId = kwargs.get("modelId")
140-
(vendor, _) = modelId.split(".")
145+
vendor, _ = parse_vendor_and_model_name_from_model_id(modelId)
141146
input_content = [
142147
{
143148
"role": message.get("role", "user"),
@@ -167,12 +172,29 @@ def traced_method(*args, **kwargs):
167172
return traced_method
168173

169174

175+
def parse_vendor_and_model_name_from_model_id(model_id):
176+
if model_id.startswith("arn:aws:bedrock:"):
177+
# This needs to be in one of the following forms:
178+
# arn:aws:bedrock:region:account-id:foundation-model/vendor.model-name
179+
# arn:aws:bedrock:region:account-id:custom-model/vendor.model-name/model-id
180+
parts = model_id.split("/")
181+
identifiers = parts[1].split(".")
182+
return identifiers[0], identifiers[1]
183+
parts = model_id.split(".")
184+
if len(parts) == 1:
185+
return parts[0], parts[0]
186+
else:
187+
return parts[-2], parts[-1]
188+
189+
170190
def patch_invoke_model(original_method, tracer, version):
171191
def traced_method(*args, **kwargs):
172192
modelId = kwargs.get("modelId")
173-
(vendor, _) = modelId.split(".")
193+
vendor, _ = parse_vendor_and_model_name_from_model_id(modelId)
174194
span_attributes = {
175195
**get_langtrace_attributes(version, vendor, vendor_type="framework"),
196+
SpanAttributes.LLM_PATH: APIS["INVOKE_MODEL"]["ENDPOINT"],
197+
SpanAttributes.LLM_IS_STREAMING: False,
176198
**get_extra_attributes(),
177199
}
178200
with tracer.start_as_current_span(
@@ -193,9 +215,11 @@ def patch_invoke_model_with_response_stream(original_method, tracer, version):
193215
@wraps(original_method)
194216
def traced_method(*args, **kwargs):
195217
modelId = kwargs.get("modelId")
196-
(vendor, _) = modelId.split(".")
218+
vendor, _ = parse_vendor_and_model_name_from_model_id(modelId)
197219
span_attributes = {
198220
**get_langtrace_attributes(version, vendor, vendor_type="framework"),
221+
SpanAttributes.LLM_PATH: APIS["INVOKE_MODEL_WITH_RESPONSE_STREAM"]["ENDPOINT"],
222+
SpanAttributes.LLM_IS_STREAMING: True,
199223
**get_extra_attributes(),
200224
}
201225
span = tracer.start_span(
@@ -217,7 +241,7 @@ def handle_streaming_call(span, kwargs, response):
217241
def stream_finished(response_body):
218242
request_body = json.loads(kwargs.get("body"))
219243

220-
(vendor, model) = kwargs.get("modelId").split(".")
244+
vendor, model = parse_vendor_and_model_name_from_model_id(kwargs.get("modelId"))
221245

222246
set_span_attribute(span, SpanAttributes.LLM_REQUEST_MODEL, model)
223247
set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, model)
@@ -241,18 +265,22 @@ def stream_finished(response_body):
241265

242266
def handle_call(span, kwargs, response):
243267
modelId = kwargs.get("modelId")
244-
(vendor, model_name) = modelId.split(".")
268+
vendor, model_name = parse_vendor_and_model_name_from_model_id(modelId)
269+
read_response_body = response.get("body").read()
270+
request_body = json.loads(kwargs.get("body"))
271+
response_body = json.loads(read_response_body)
245272
response["body"] = BufferedStreamBody(
246-
response["body"]._raw_stream, response["body"]._content_length
273+
io.BytesIO(read_response_body), len(read_response_body)
247274
)
248-
request_body = json.loads(kwargs.get("body"))
249-
response_body = json.loads(response.get("body").read())
250275

251276
set_span_attribute(span, SpanAttributes.LLM_RESPONSE_MODEL, modelId)
252277
set_span_attribute(span, SpanAttributes.LLM_REQUEST_MODEL, modelId)
253278

254279
if vendor == "amazon":
255-
set_amazon_attributes(span, request_body, response_body)
280+
if model_name.startswith("titan-embed-text"):
281+
set_amazon_embedding_attributes(span, request_body, response_body)
282+
else:
283+
set_amazon_attributes(span, request_body, response_body)
256284

257285
if vendor == "anthropic":
258286
if "prompt" in request_body:
@@ -356,6 +384,27 @@ def set_amazon_attributes(span, request_body, response_body):
356384
set_event_completion(span, completions)
357385

358386

387+
def set_amazon_embedding_attributes(span, request_body, response_body):
388+
input_text = request_body.get("inputText")
389+
set_event_prompt(span, input_text)
390+
391+
embeddings = response_body.get("embedding", [])
392+
input_tokens = response_body.get("inputTextTokenCount")
393+
set_usage_attributes(
394+
span,
395+
{
396+
"input_tokens": input_tokens,
397+
"output": len(embeddings),
398+
},
399+
)
400+
set_span_attribute(
401+
span, SpanAttributes.LLM_REQUEST_MODEL, request_body.get("modelId")
402+
)
403+
set_span_attribute(
404+
span, SpanAttributes.LLM_RESPONSE_MODEL, request_body.get("modelId")
405+
)
406+
407+
359408
def set_anthropic_completions_attributes(span, request_body, response_body):
360409
set_span_attribute(
361410
span,
@@ -442,10 +491,10 @@ def _set_response_attributes(span, kwargs, result):
442491
)
443492

444493

445-
def set_span_streaming_response(span, response):
494+
def set_span_streaming_response(span, response_stream):
446495
streaming_response = ""
447496
role = None
448-
for event in response["stream"]:
497+
for event in response_stream:
449498
if "messageStart" in event:
450499
role = event["messageStart"]["role"]
451500
elif "contentBlockDelta" in event:
@@ -475,13 +524,15 @@ def __init__(
475524
stream_done_callback=None,
476525
):
477526
super().__init__(response)
478-
479527
self._stream_done_callback = stream_done_callback
480528
self._accumulating_body = {"generation": ""}
529+
self.last_chunk = None
481530

482531
def __iter__(self):
483532
for event in self.__wrapped__:
533+
# Process the event
484534
self._process_event(event)
535+
# Yield the original event immediately
485536
yield event
486537

487538
def _process_event(self, event):
@@ -496,7 +547,11 @@ def _process_event(self, event):
496547
self._stream_done_callback(decoded_chunk)
497548
return
498549
if "generation" in decoded_chunk:
499-
self._accumulating_body["generation"] += decoded_chunk.get("generation")
550+
generation = decoded_chunk.get("generation")
551+
if self.last_chunk == generation:
552+
return
553+
self.last_chunk = generation
554+
self._accumulating_body["generation"] += generation
500555

501556
if type == "message_start":
502557
self._accumulating_body = decoded_chunk.get("message")
@@ -505,9 +560,11 @@ def _process_event(self, event):
505560
decoded_chunk.get("content_block")
506561
)
507562
elif type == "content_block_delta":
508-
self._accumulating_body["content"][-1]["text"] += decoded_chunk.get(
509-
"delta"
510-
).get("text")
563+
text = decoded_chunk.get("delta").get("text")
564+
if self.last_chunk == text:
565+
return
566+
self.last_chunk = text
567+
self._accumulating_body["content"][-1]["text"] += text
511568

512569
elif self.has_finished(type, decoded_chunk):
513570
self._accumulating_body["invocation_metrics"] = decoded_chunk.get(

src/langtrace_python_sdk/instrumentation/pinecone/instrumentation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class PineconeInstrumentation(BaseInstrumentor):
3333
The PineconeInstrumentation class represents the Pinecone instrumentation"""
3434

3535
def instrumentation_dependencies(self) -> Collection[str]:
36-
return ["pinecone >= 3.1.0"]
36+
return ["pinecone >= 3.1.0", "pinecone <= 6.0.2"]
3737

3838
def _instrument(self, **kwargs):
3939
tracer_provider = kwargs.get("tracer_provider")
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "3.8.18"
1+
__version__ = "3.8.21"
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
interactions:
2+
- request:
3+
body: '{"messages": [{"role": "user", "content": "Say this is a test three times"}],
4+
"anthropic_version": "bedrock-2023-05-31", "max_tokens": 100}'
5+
headers:
6+
Accept:
7+
- !!binary |
8+
YXBwbGljYXRpb24vanNvbg==
9+
Content-Length:
10+
- '139'
11+
Content-Type:
12+
- !!binary |
13+
YXBwbGljYXRpb24vanNvbg==
14+
User-Agent:
15+
- !!binary |
16+
Qm90bzMvMS4zOC4xOCBtZC9Cb3RvY29yZSMxLjM4LjE4IHVhLzIuMSBvcy9tYWNvcyMyNC40LjAg
17+
bWQvYXJjaCNhcm02NCBsYW5nL3B5dGhvbiMzLjEzLjEgbWQvcHlpbXBsI0NQeXRob24gbS9aLGIg
18+
Y2ZnL3JldHJ5LW1vZGUjc3RhbmRhcmQgQm90b2NvcmUvMS4zOC4xOA==
19+
method: POST
20+
uri: https://bedrock-runtime.us-east-1.amazonaws.com/model/us.anthropic.claude-3-7-sonnet-20250219-v1%3A0/invoke
21+
response:
22+
body:
23+
string: '{"id":"msg_bdrk_01NJB1bDTLkFh6pgfoAD5hkb","type":"message","role":"assistant","model":"claude-3-7-sonnet-20250219","content":[{"type":"text","text":"This
24+
is a test.\nThis is a test.\nThis is a test."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":14,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":20}}'
25+
headers:
26+
Connection:
27+
- keep-alive
28+
Content-Length:
29+
- '355'
30+
Content-Type:
31+
- application/json
32+
Date:
33+
- Mon, 19 May 2025 16:42:05 GMT
34+
X-Amzn-Bedrock-Input-Token-Count:
35+
- '14'
36+
X-Amzn-Bedrock-Invocation-Latency:
37+
- '926'
38+
X-Amzn-Bedrock-Output-Token-Count:
39+
- '20'
40+
x-amzn-RequestId:
41+
- c0a92363-ec28-4a8b-9c09-571131d946b0
42+
status:
43+
code: 200
44+
message: OK
45+
version: 1

src/tests/aws_bedrock/cassettes/test_generate_embedding.yaml

Lines changed: 41 additions & 0 deletions
Large diffs are not rendered by default.

src/tests/aws_bedrock/conftest.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""Unit tests configuration module."""
2+
3+
import pytest
4+
import os
5+
6+
from boto3.session import Session
7+
from botocore.config import Config
8+
9+
from langtrace_python_sdk.instrumentation.aws_bedrock.instrumentation import (
10+
AWSBedrockInstrumentation,
11+
)
12+
13+
14+
@pytest.fixture(autouse=True)
15+
def environment():
16+
if not os.getenv("AWS_ACCESS_KEY_ID"):
17+
os.environ["AWS_ACCESS_KEY_ID"] = "test_aws_access_key_id"
18+
if not os.getenv("AWS_SECRET_ACCESS_KEY"):
19+
os.environ["AWS_SECRET_ACCESS_KEY"] = "test_aws_secret_access_key"
20+
21+
22+
@pytest.fixture
23+
def aws_bedrock_client():
24+
bedrock_config = Config(
25+
region_name="us-east-1",
26+
connect_timeout=300,
27+
read_timeout=300,
28+
retries={"total_max_attempts": 2, "mode": "standard"},
29+
)
30+
return Session().client("bedrock-runtime", config=bedrock_config)
31+
32+
33+
@pytest.fixture(scope="module")
34+
def vcr_config():
35+
return {
36+
"filter_headers": [
37+
"authorization",
38+
"X-Amz-Date",
39+
"X-Amz-Security-Token",
40+
"amz-sdk-invocation-id",
41+
"amz-sdk-request",
42+
]
43+
}
44+
45+
46+
@pytest.fixture(scope="session", autouse=True)
47+
def instrument():
48+
AWSBedrockInstrumentation().instrument()

0 commit comments

Comments
 (0)