1818
1919from __future__ import annotations
2020
21+ import io
22+ import json
2123import logging
2224from typing import Any
2325
26+ from botocore .response import StreamingBody
27+
2428from opentelemetry .instrumentation .botocore .extensions .types import (
2529 _AttributeMapT ,
2630 _AwsSdkExtension ,
@@ -58,7 +62,7 @@ class _BedrockRuntimeExtension(_AwsSdkExtension):
5862 Amazon Bedrock Runtime</a>.
5963 """
6064
61- _HANDLED_OPERATIONS = {"Converse" }
65+ _HANDLED_OPERATIONS = {"Converse" , "InvokeModel" }
6266
6367 def extract_attributes (self , attributes : _AttributeMapT ):
6468 if self ._call_context .operation not in self ._HANDLED_OPERATIONS :
@@ -73,6 +77,7 @@ def extract_attributes(self, attributes: _AttributeMapT):
7377 GenAiOperationNameValues .CHAT .value
7478 )
7579
80+ # Converse
7681 if inference_config := self ._call_context .params .get (
7782 "inferenceConfig"
7883 ):
@@ -97,6 +102,84 @@ def extract_attributes(self, attributes: _AttributeMapT):
97102 inference_config .get ("stopSequences" ),
98103 )
99104
105+ # InvokeModel
106+ # Get the request body if it exists
107+ body = self ._call_context .params .get ("body" )
108+ if body :
109+ try :
110+ request_body = json .loads (body )
111+
112+ if "amazon.titan" in model_id :
113+ # titan interface is a text completion one
114+ attributes [GEN_AI_OPERATION_NAME ] = (
115+ GenAiOperationNameValues .TEXT_COMPLETION .value
116+ )
117+ self ._extract_titan_attributes (
118+ attributes , request_body
119+ )
120+ elif "amazon.nova" in model_id :
121+ self ._extract_nova_attributes (attributes , request_body )
122+ elif "anthropic.claude" in model_id :
123+ self ._extract_claude_attributes (
124+ attributes , request_body
125+ )
126+ except json .JSONDecodeError :
127+ _logger .debug ("Error: Unable to parse the body as JSON" )
128+
129+ def _extract_titan_attributes (self , attributes , request_body ):
130+ config = request_body .get ("textGenerationConfig" , {})
131+ self ._set_if_not_none (
132+ attributes , GEN_AI_REQUEST_TEMPERATURE , config .get ("temperature" )
133+ )
134+ self ._set_if_not_none (
135+ attributes , GEN_AI_REQUEST_TOP_P , config .get ("topP" )
136+ )
137+ self ._set_if_not_none (
138+ attributes , GEN_AI_REQUEST_MAX_TOKENS , config .get ("maxTokenCount" )
139+ )
140+ self ._set_if_not_none (
141+ attributes ,
142+ GEN_AI_REQUEST_STOP_SEQUENCES ,
143+ config .get ("stopSequences" ),
144+ )
145+
146+ def _extract_nova_attributes (self , attributes , request_body ):
147+ config = request_body .get ("inferenceConfig" , {})
148+ self ._set_if_not_none (
149+ attributes , GEN_AI_REQUEST_TEMPERATURE , config .get ("temperature" )
150+ )
151+ self ._set_if_not_none (
152+ attributes , GEN_AI_REQUEST_TOP_P , config .get ("topP" )
153+ )
154+ self ._set_if_not_none (
155+ attributes , GEN_AI_REQUEST_MAX_TOKENS , config .get ("max_new_tokens" )
156+ )
157+ self ._set_if_not_none (
158+ attributes ,
159+ GEN_AI_REQUEST_STOP_SEQUENCES ,
160+ config .get ("stopSequences" ),
161+ )
162+
163+ def _extract_claude_attributes (self , attributes , request_body ):
164+ self ._set_if_not_none (
165+ attributes ,
166+ GEN_AI_REQUEST_MAX_TOKENS ,
167+ request_body .get ("max_tokens" ),
168+ )
169+ self ._set_if_not_none (
170+ attributes ,
171+ GEN_AI_REQUEST_TEMPERATURE ,
172+ request_body .get ("temperature" ),
173+ )
174+ self ._set_if_not_none (
175+ attributes , GEN_AI_REQUEST_TOP_P , request_body .get ("top_p" )
176+ )
177+ self ._set_if_not_none (
178+ attributes ,
179+ GEN_AI_REQUEST_STOP_SEQUENCES ,
180+ request_body .get ("stop_sequences" ),
181+ )
182+
100183 @staticmethod
101184 def _set_if_not_none (attributes , key , value ):
102185 if value is not None :
@@ -115,13 +198,8 @@ def before_service_call(self, span: Span):
115198 if operation_name and request_model :
116199 span .update_name (f"{ operation_name } { request_model } " )
117200
118- def on_success (self , span : Span , result : dict [str , Any ]):
119- if self ._call_context .operation not in self ._HANDLED_OPERATIONS :
120- return
121-
122- if not span .is_recording ():
123- return
124-
201+ # pylint: disable=no-self-use
202+ def _converse_on_success (self , span : Span , result : dict [str , Any ]):
125203 if usage := result .get ("usage" ):
126204 if input_tokens := usage .get ("inputTokens" ):
127205 span .set_attribute (
@@ -140,6 +218,109 @@ def on_success(self, span: Span, result: dict[str, Any]):
140218 [stop_reason ],
141219 )
142220
221+ def _invoke_model_on_success (
222+ self , span : Span , result : dict [str , Any ], model_id : str
223+ ):
224+ original_body = None
225+ try :
226+ original_body = result ["body" ]
227+ body_content = original_body .read ()
228+
229+ # Replenish stream for downstream application use
230+ new_stream = io .BytesIO (body_content )
231+ result ["body" ] = StreamingBody (new_stream , len (body_content ))
232+
233+ response_body = json .loads (body_content .decode ("utf-8" ))
234+ if "amazon.titan" in model_id :
235+ self ._handle_amazon_titan_response (span , response_body )
236+ elif "amazon.nova" in model_id :
237+ self ._handle_amazon_nova_response (span , response_body )
238+ elif "anthropic.claude" in model_id :
239+ self ._handle_anthropic_claude_response (span , response_body )
240+
241+ except json .JSONDecodeError :
242+ _logger .debug ("Error: Unable to parse the response body as JSON" )
243+ except Exception as exc : # pylint: disable=broad-exception-caught
244+ _logger .debug ("Error processing response: %s" , exc )
245+ finally :
246+ if original_body is not None :
247+ original_body .close ()
248+
249+ def on_success (self , span : Span , result : dict [str , Any ]):
250+ if self ._call_context .operation not in self ._HANDLED_OPERATIONS :
251+ return
252+
253+ if not span .is_recording ():
254+ return
255+
256+ # Converse
257+ self ._converse_on_success (span , result )
258+
259+ model_id = self ._call_context .params .get (_MODEL_ID_KEY )
260+ if not model_id :
261+ return
262+
263+ # InvokeModel
264+ if "body" in result and isinstance (result ["body" ], StreamingBody ):
265+ self ._invoke_model_on_success (span , result , model_id )
266+
267+ # pylint: disable=no-self-use
268+ def _handle_amazon_titan_response (
269+ self , span : Span , response_body : dict [str , Any ]
270+ ):
271+ if "inputTextTokenCount" in response_body :
272+ span .set_attribute (
273+ GEN_AI_USAGE_INPUT_TOKENS , response_body ["inputTextTokenCount" ]
274+ )
275+ if "results" in response_body and response_body ["results" ]:
276+ result = response_body ["results" ][0 ]
277+ if "tokenCount" in result :
278+ span .set_attribute (
279+ GEN_AI_USAGE_OUTPUT_TOKENS , result ["tokenCount" ]
280+ )
281+ if "completionReason" in result :
282+ span .set_attribute (
283+ GEN_AI_RESPONSE_FINISH_REASONS ,
284+ [result ["completionReason" ]],
285+ )
286+
287+ # pylint: disable=no-self-use
288+ def _handle_amazon_nova_response (
289+ self , span : Span , response_body : dict [str , Any ]
290+ ):
291+ if "usage" in response_body :
292+ usage = response_body ["usage" ]
293+ if "inputTokens" in usage :
294+ span .set_attribute (
295+ GEN_AI_USAGE_INPUT_TOKENS , usage ["inputTokens" ]
296+ )
297+ if "outputTokens" in usage :
298+ span .set_attribute (
299+ GEN_AI_USAGE_OUTPUT_TOKENS , usage ["outputTokens" ]
300+ )
301+ if "stopReason" in response_body :
302+ span .set_attribute (
303+ GEN_AI_RESPONSE_FINISH_REASONS , [response_body ["stopReason" ]]
304+ )
305+
306+ # pylint: disable=no-self-use
307+ def _handle_anthropic_claude_response (
308+ self , span : Span , response_body : dict [str , Any ]
309+ ):
310+ if usage := response_body .get ("usage" ):
311+ if "input_tokens" in usage :
312+ span .set_attribute (
313+ GEN_AI_USAGE_INPUT_TOKENS , usage ["input_tokens" ]
314+ )
315+ if "output_tokens" in usage :
316+ span .set_attribute (
317+ GEN_AI_USAGE_OUTPUT_TOKENS , usage ["output_tokens" ]
318+ )
319+ if "stop_reason" in response_body :
320+ span .set_attribute (
321+ GEN_AI_RESPONSE_FINISH_REASONS , [response_body ["stop_reason" ]]
322+ )
323+
143324 def on_error (self , span : Span , exception : _BotoClientErrorT ):
144325 if self ._call_context .operation not in self ._HANDLED_OPERATIONS :
145326 return
0 commit comments