18
18
19
19
from __future__ import annotations
20
20
21
+ import io
22
+ import json
21
23
import logging
22
24
from typing import Any
23
25
26
+ from botocore .response import StreamingBody
27
+
24
28
from opentelemetry .instrumentation .botocore .extensions .types import (
25
29
_AttributeMapT ,
26
30
_AwsSdkExtension ,
@@ -58,7 +62,7 @@ class _BedrockRuntimeExtension(_AwsSdkExtension):
58
62
Amazon Bedrock Runtime</a>.
59
63
"""
60
64
61
- _HANDLED_OPERATIONS = {"Converse" }
65
+ _HANDLED_OPERATIONS = {"Converse" , "InvokeModel" }
62
66
63
67
def extract_attributes (self , attributes : _AttributeMapT ):
64
68
if self ._call_context .operation not in self ._HANDLED_OPERATIONS :
@@ -73,6 +77,7 @@ def extract_attributes(self, attributes: _AttributeMapT):
73
77
GenAiOperationNameValues .CHAT .value
74
78
)
75
79
80
+ # Converse
76
81
if inference_config := self ._call_context .params .get (
77
82
"inferenceConfig"
78
83
):
@@ -97,6 +102,84 @@ def extract_attributes(self, attributes: _AttributeMapT):
97
102
inference_config .get ("stopSequences" ),
98
103
)
99
104
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
+
100
183
@staticmethod
101
184
def _set_if_not_none (attributes , key , value ):
102
185
if value is not None :
@@ -115,13 +198,8 @@ def before_service_call(self, span: Span):
115
198
if operation_name and request_model :
116
199
span .update_name (f"{ operation_name } { request_model } " )
117
200
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 ]):
125
203
if usage := result .get ("usage" ):
126
204
if input_tokens := usage .get ("inputTokens" ):
127
205
span .set_attribute (
@@ -140,6 +218,109 @@ def on_success(self, span: Span, result: dict[str, Any]):
140
218
[stop_reason ],
141
219
)
142
220
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
+
143
324
def on_error (self , span : Span , exception : _BotoClientErrorT ):
144
325
if self ._call_context .operation not in self ._HANDLED_OPERATIONS :
145
326
return
0 commit comments