Skip to content

Commit f8e96cd

Browse files
committed
Added bedrock support and test
1 parent 38179c9 commit f8e96cd

File tree

5 files changed

+268
-54
lines changed

5 files changed

+268
-54
lines changed

instrumentation-genai/opentelemetry-instrumentation-langchain/src/opentelemetry/instrumentation/langchain/callback_handler.py

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -54,43 +54,66 @@ def on_chat_model_start(
5454
metadata: dict[str, Any] | None,
5555
**kwargs: Any,
5656
) -> None:
57-
invocation_params = kwargs.get("invocation_params")
58-
request_model = (
59-
invocation_params.get("model_name") if invocation_params else ""
60-
)
61-
span = self.span_manager.create_llm_span(
57+
if "invocation_params" in kwargs:
58+
params = (
59+
kwargs["invocation_params"].get("params")
60+
or kwargs["invocation_params"]
61+
)
62+
else:
63+
params = kwargs
64+
65+
request_model = "unknown"
66+
for model_tag in ("model", "model_id", "model_name", "ls_model_name"):
67+
if (model := kwargs.get(model_tag)) is not None:
68+
request_model = model
69+
break
70+
elif (
71+
model := (params or {}).get(model_tag)
72+
) is not None:
73+
request_model = model
74+
break
75+
elif (model := (metadata or {}).get(model_tag)) is not None:
76+
request_model = model
77+
break
78+
79+
span = self.span_manager.create_chat_span(
6280
run_id=run_id,
6381
parent_run_id=parent_run_id,
6482
request_model=request_model,
6583
)
6684

67-
if invocation_params is not None:
68-
top_p = invocation_params.get("top_p")
85+
if params is not None:
86+
top_p = params.get("top_p")
6987
if top_p is not None:
7088
span.set_attribute(GenAI.GEN_AI_REQUEST_TOP_P, top_p)
71-
frequency_penalty = invocation_params.get("frequency_penalty")
89+
frequency_penalty = params.get("frequency_penalty")
7290
if frequency_penalty is not None:
7391
span.set_attribute(
7492
GenAI.GEN_AI_REQUEST_FREQUENCY_PENALTY, frequency_penalty
7593
)
76-
presence_penalty = invocation_params.get("presence_penalty")
94+
presence_penalty = params.get("presence_penalty")
7795
if presence_penalty is not None:
7896
span.set_attribute(
7997
GenAI.GEN_AI_REQUEST_PRESENCE_PENALTY, presence_penalty
8098
)
81-
stop_sequences = invocation_params.get("stop")
99+
stop_sequences = params.get("stop")
82100
if stop_sequences is not None:
83101
span.set_attribute(
84102
GenAI.GEN_AI_REQUEST_STOP_SEQUENCES, stop_sequences
85103
)
86-
seed = invocation_params.get("seed")
104+
seed = params.get("seed")
87105
if seed is not None:
88106
span.set_attribute(GenAI.GEN_AI_REQUEST_SEED, seed)
89-
90-
if metadata is not None:
91-
max_tokens = metadata.get("ls_max_tokens")
107+
temperature = params.get("temperature")
108+
if temperature is not None:
109+
span.set_attribute(
110+
GenAI.GEN_AI_REQUEST_TEMPERATURE, temperature
111+
)
112+
max_tokens = params.get("max_completion_tokens")
92113
if max_tokens is not None:
93114
span.set_attribute(GenAI.GEN_AI_REQUEST_MAX_TOKENS, max_tokens)
115+
116+
if metadata is not None:
94117
provider = metadata.get("ls_provider")
95118
if provider is not None:
96119
span.set_attribute("gen_ai.provider.name", provider)
@@ -99,6 +122,9 @@ def on_chat_model_start(
99122
span.set_attribute(
100123
GenAI.GEN_AI_REQUEST_TEMPERATURE, temperature
101124
)
125+
max_tokens = metadata.get("ls_max_tokens")
126+
if max_tokens is not None:
127+
span.set_attribute(GenAI.GEN_AI_REQUEST_MAX_TOKENS, max_tokens)
102128

103129
def on_llm_end(
104130
self,
@@ -124,6 +150,35 @@ def on_llm_end(
124150
finish_reason = generation_info.get("finish_reason")
125151
if finish_reason is not None:
126152
finish_reasons.append(str(finish_reason) or "error")
153+
if chat_generation.message:
154+
if (
155+
generation_info is None
156+
and chat_generation.message.response_metadata
157+
):
158+
finish_reason = (
159+
chat_generation.message.response_metadata.get(
160+
"stopReason"
161+
)
162+
)
163+
if finish_reason is not None and span.is_recording():
164+
finish_reasons.append(finish_reason or "error")
165+
if chat_generation.message.usage_metadata:
166+
input_tokens = (
167+
chat_generation.message.usage_metadata.get(
168+
"input_tokens", 0
169+
)
170+
)
171+
output_tokens = (
172+
chat_generation.message.usage_metadata.get(
173+
"output_tokens", 0
174+
)
175+
)
176+
span.set_attribute(
177+
GenAI.GEN_AI_USAGE_INPUT_TOKENS, input_tokens
178+
)
179+
span.set_attribute(
180+
GenAI.GEN_AI_USAGE_OUTPUT_TOKENS, output_tokens
181+
)
127182

128183
span.set_attribute(
129184
GenAI.GEN_AI_RESPONSE_FINISH_REASONS, finish_reasons
@@ -143,22 +198,6 @@ def on_llm_end(
143198
if response_id is not None:
144199
span.set_attribute(GenAI.GEN_AI_RESPONSE_ID, str(response_id))
145200

146-
# usage
147-
usage = llm_output.get("usage") or llm_output.get("token_usage")
148-
if usage:
149-
prompt_tokens = usage.get("prompt_tokens", 0)
150-
completion_tokens = usage.get("completion_tokens", 0)
151-
span.set_attribute(
152-
GenAI.GEN_AI_USAGE_INPUT_TOKENS,
153-
int(prompt_tokens) if prompt_tokens is not None else 0,
154-
)
155-
span.set_attribute(
156-
GenAI.GEN_AI_USAGE_OUTPUT_TOKENS,
157-
int(completion_tokens)
158-
if completion_tokens is not None
159-
else 0,
160-
)
161-
162201
# End the LLM span
163202
self.span_manager.end_span(run_id)
164203

instrumentation-genai/opentelemetry-instrumentation-langchain/tests/cassettes/test_langchain_call.yaml renamed to instrumentation-genai/opentelemetry-instrumentation-langchain/tests/cassettes/test_chat_openai_gpt_3_5_turbo_model_llm_call.yaml

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ interactions:
4242
host:
4343
- api.openai.com
4444
user-agent:
45-
- OpenAI/Python 1.98.0
45+
- OpenAI/Python 1.105.0
4646
x-stainless-arch:
4747
- arm64
4848
x-stainless-async:
@@ -52,7 +52,9 @@ interactions:
5252
x-stainless-os:
5353
- MacOS
5454
x-stainless-package-version:
55-
- 1.98.0
55+
- 1.105.0
56+
x-stainless-raw-response:
57+
- 'true'
5658
x-stainless-retry-count:
5759
- '0'
5860
x-stainless-runtime:
@@ -65,9 +67,9 @@ interactions:
6567
body:
6668
string: |-
6769
{
68-
"id": "chatcmpl-BzXyZbwSwCmuSOKwRx7tnsia3V9lH",
70+
"id": "chatcmpl-CBmRYRvfmoAG6EmqDOH4IwkfN02MJ",
6971
"object": "chat.completion",
70-
"created": 1754008311,
72+
"created": 1756923860,
7173
"model": "gpt-3.5-turbo-0125",
7274
"choices": [
7375
{
@@ -102,13 +104,13 @@ interactions:
102104
}
103105
headers:
104106
CF-RAY:
105-
- 96813c201ae62f46-LAX
107+
- 9797488a68f42b63-LAX
106108
Connection:
107109
- keep-alive
108110
Content-Type:
109111
- application/json
110112
Date:
111-
- Fri, 01 Aug 2025 00:31:51 GMT
113+
- Wed, 03 Sep 2025 18:24:21 GMT
112114
Server:
113115
- cloudflare
114116
Set-Cookie: test_set_cookie
@@ -128,29 +130,27 @@ interactions:
128130
- '822'
129131
openai-organization: test_openai_org_id
130132
openai-processing-ms:
131-
- '189'
133+
- '783'
132134
openai-project:
133-
- proj_GLiYlAc06hF0Fm06IMReZLy4
135+
- proj_3o0Aqh32nPiGbrex8BJtPTCm
134136
openai-version:
135137
- '2020-10-01'
136-
x-envoy-decorator-operation:
137-
- router.openai.svc.cluster.local:5004/*
138138
x-envoy-upstream-service-time:
139-
- '211'
139+
- '787'
140140
x-ratelimit-limit-requests:
141-
- '10000'
141+
- '5000'
142142
x-ratelimit-limit-tokens:
143-
- '200000'
143+
- '2000000'
144144
x-ratelimit-remaining-requests:
145-
- '9999'
145+
- '4999'
146146
x-ratelimit-remaining-tokens:
147-
- '199982'
147+
- '1999982'
148148
x-ratelimit-reset-requests:
149-
- 8.64s
149+
- 12ms
150150
x-ratelimit-reset-tokens:
151-
- 5ms
151+
- 0s
152152
x-request-id:
153-
- 8a5db9a5-6103-41d4-baf3-813be646bae4
153+
- req_c1bd8705b06b4e9180a5d8340e44785c
154154
status:
155155
code: 200
156156
message: OK
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
interactions:
2+
- request:
3+
body: |-
4+
{
5+
"messages": [
6+
{
7+
"role": "user",
8+
"content": [
9+
{
10+
"text": "What is the capital of France?"
11+
}
12+
]
13+
}
14+
],
15+
"system": [
16+
{
17+
"text": "You are a helpful assistant!"
18+
}
19+
],
20+
"inferenceConfig": {
21+
"maxTokens": 100,
22+
"temperature": 0.1
23+
}
24+
}
25+
headers:
26+
Content-Length:
27+
- '202'
28+
Content-Type:
29+
- !!binary |
30+
YXBwbGljYXRpb24vanNvbg==
31+
User-Agent:
32+
- !!binary |
33+
Qm90bzMvMS40MC4yMiBtZC9Cb3RvY29yZSMxLjQwLjIyIHVhLzIuMSBvcy9tYWNvcyMyNC42LjAg
34+
bWQvYXJjaCNhcm02NCBsYW5nL3B5dGhvbiMzLjEzLjUgbWQvcHlpbXBsI0NQeXRob24gbS9aLGIs
35+
RCBjZmcvcmV0cnktbW9kZSNsZWdhY3kgQm90b2NvcmUvMS40MC4yMg==
36+
X-Amz-Date:
37+
- !!binary |
38+
MjAyNTA5MDRUMDIyNzM4Wg==
39+
amz-sdk-invocation-id:
40+
- !!binary |
41+
MTMwMjBiMWUtZDhkOC00NTNkLWI1ZjYtY2U5Yjk5ZWQ4Zjg4
42+
amz-sdk-request:
43+
- !!binary |
44+
YXR0ZW1wdD0x
45+
authorization:
46+
- Bearer test_openai_api_key
47+
method: POST
48+
uri: https://bedrock-runtime.us-west-2.amazonaws.com/model/arn%3Aaws%3Abedrock%3Aus-west-2%3A906383545488%3Ainference-profile%2Fus.amazon.nova-lite-v1%3A0/converse
49+
response:
50+
body:
51+
string: |-
52+
{
53+
"metrics": {
54+
"latencyMs": 435
55+
},
56+
"output": {
57+
"message": {
58+
"content": [
59+
{
60+
"text": "The capital of France is Paris. It is not only the capital but also the largest city in the country. Paris is known for its rich history, culture, and landmarks such as the Eiffel Tower, the Louvre Museum, and Notre-Dame Cathedral."
61+
}
62+
],
63+
"role": "assistant"
64+
}
65+
},
66+
"stopReason": "end_turn",
67+
"usage": {
68+
"inputTokens": 13,
69+
"outputTokens": 50,
70+
"totalTokens": 63
71+
}
72+
}
73+
headers:
74+
Connection:
75+
- keep-alive
76+
Content-Length:
77+
- '412'
78+
Content-Type:
79+
- application/json
80+
Date:
81+
- Thu, 04 Sep 2025 02:27:38 GMT
82+
Set-Cookie: test_set_cookie
83+
openai-organization: test_openai_org_id
84+
x-amzn-RequestId:
85+
- 38e8f9b0-89f9-4d78-8da4-bba3948f1889
86+
status:
87+
code: 200
88+
message: OK
89+
version: 1

instrumentation-genai/opentelemetry-instrumentation-langchain/tests/conftest.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import json
44
import os
55

6+
import boto3
67
import pytest
78
import yaml
9+
from langchain_aws import ChatBedrock
810
from langchain_openai import ChatOpenAI
911

1012
from opentelemetry.instrumentation.langchain import LangChainInstrumentor
@@ -15,8 +17,8 @@
1517
)
1618

1719

18-
@pytest.fixture(scope="function", name="llm_model")
19-
def fixture_llm_model():
20+
@pytest.fixture(scope="function", name="chat_openai_gpt_3_5_turbo_model")
21+
def fixture_chat_openai_gpt_3_5_turbo_model():
2022
llm = ChatOpenAI(
2123
model="gpt-3.5-turbo",
2224
temperature=0.1,
@@ -30,6 +32,25 @@ def fixture_llm_model():
3032
yield llm
3133

3234

35+
@pytest.fixture(scope="function", name="us_amazon_nova_lite_v1_0")
36+
def fixture_us_amazon_nova_lite_v1_0():
37+
llm_model_value = "arn:aws:bedrock:us-west-2:906383545488:inference-profile/us.amazon.nova-lite-v1:0"
38+
llm = ChatBedrock(
39+
model_id=llm_model_value,
40+
client=boto3.client(
41+
"bedrock-runtime",
42+
aws_access_key_id="test_key",
43+
aws_secret_access_key="test_secret",
44+
region_name="us-west-2",
45+
aws_account_id="test_account",
46+
),
47+
provider="amazon",
48+
temperature=0.1,
49+
max_tokens=100,
50+
)
51+
yield llm
52+
53+
3354
@pytest.fixture(scope="function", name="span_exporter")
3455
def fixture_span_exporter():
3556
exporter = InMemorySpanExporter()

0 commit comments

Comments
 (0)