payi 0.1.0a110__py3-none-any.whl → 0.1.0a137__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. payi/__init__.py +3 -1
  2. payi/_base_client.py +12 -12
  3. payi/_client.py +8 -8
  4. payi/_compat.py +48 -48
  5. payi/_models.py +87 -59
  6. payi/_qs.py +7 -7
  7. payi/_streaming.py +4 -6
  8. payi/_types.py +53 -12
  9. payi/_utils/__init__.py +9 -2
  10. payi/_utils/_compat.py +45 -0
  11. payi/_utils/_datetime_parse.py +136 -0
  12. payi/_utils/_sync.py +3 -31
  13. payi/_utils/_transform.py +13 -3
  14. payi/_utils/_typing.py +6 -1
  15. payi/_utils/_utils.py +5 -6
  16. payi/_version.py +1 -1
  17. payi/lib/AnthropicInstrumentor.py +83 -57
  18. payi/lib/BedrockInstrumentor.py +292 -57
  19. payi/lib/GoogleGenAiInstrumentor.py +18 -31
  20. payi/lib/OpenAIInstrumentor.py +56 -72
  21. payi/lib/ProviderRequest.py +216 -0
  22. payi/lib/StreamWrappers.py +379 -0
  23. payi/lib/VertexInstrumentor.py +18 -37
  24. payi/lib/VertexRequest.py +16 -2
  25. payi/lib/data/cohere_embed_english_v3.json +30706 -0
  26. payi/lib/helpers.py +53 -1
  27. payi/lib/instrument.py +404 -668
  28. payi/resources/categories/__init__.py +0 -14
  29. payi/resources/categories/categories.py +25 -53
  30. payi/resources/categories/resources.py +27 -23
  31. payi/resources/ingest.py +126 -132
  32. payi/resources/limits/__init__.py +14 -14
  33. payi/resources/limits/limits.py +58 -58
  34. payi/resources/limits/properties.py +171 -0
  35. payi/resources/requests/request_id/properties.py +8 -8
  36. payi/resources/requests/request_id/result.py +3 -3
  37. payi/resources/requests/response_id/properties.py +8 -8
  38. payi/resources/requests/response_id/result.py +3 -3
  39. payi/resources/use_cases/definitions/definitions.py +27 -27
  40. payi/resources/use_cases/definitions/kpis.py +23 -23
  41. payi/resources/use_cases/definitions/limit_config.py +14 -14
  42. payi/resources/use_cases/definitions/version.py +3 -3
  43. payi/resources/use_cases/kpis.py +15 -15
  44. payi/resources/use_cases/properties.py +6 -6
  45. payi/resources/use_cases/use_cases.py +7 -7
  46. payi/types/__init__.py +2 -0
  47. payi/types/bulk_ingest_response.py +3 -20
  48. payi/types/categories/__init__.py +0 -1
  49. payi/types/categories/resource_list_params.py +5 -1
  50. payi/types/category_list_resources_params.py +5 -1
  51. payi/types/category_resource_response.py +31 -1
  52. payi/types/ingest_event_param.py +7 -6
  53. payi/types/ingest_units_params.py +5 -4
  54. payi/types/limit_create_params.py +3 -3
  55. payi/types/limit_list_response.py +1 -3
  56. payi/types/limit_response.py +1 -3
  57. payi/types/limits/__init__.py +2 -9
  58. payi/types/limits/{tag_remove_params.py → property_update_params.py} +4 -5
  59. payi/types/limits/{tag_delete_response.py → property_update_response.py} +3 -3
  60. payi/types/requests/request_id/property_update_params.py +2 -2
  61. payi/types/requests/response_id/property_update_params.py +2 -2
  62. payi/types/shared/__init__.py +2 -0
  63. payi/types/shared/api_error.py +18 -0
  64. payi/types/shared/pay_i_common_models_budget_management_create_limit_base.py +3 -3
  65. payi/types/shared/properties_request.py +11 -0
  66. payi/types/shared/xproxy_result.py +2 -0
  67. payi/types/shared_params/pay_i_common_models_budget_management_create_limit_base.py +3 -3
  68. payi/types/use_cases/definitions/limit_config_create_params.py +3 -3
  69. payi/types/use_cases/property_update_params.py +2 -2
  70. {payi-0.1.0a110.dist-info → payi-0.1.0a137.dist-info}/METADATA +6 -6
  71. {payi-0.1.0a110.dist-info → payi-0.1.0a137.dist-info}/RECORD +73 -75
  72. payi/resources/categories/fixed_cost_resources.py +0 -196
  73. payi/resources/limits/tags.py +0 -507
  74. payi/types/categories/fixed_cost_resource_create_params.py +0 -21
  75. payi/types/limits/limit_tags.py +0 -16
  76. payi/types/limits/tag_create_params.py +0 -13
  77. payi/types/limits/tag_create_response.py +0 -10
  78. payi/types/limits/tag_list_response.py +0 -10
  79. payi/types/limits/tag_remove_response.py +0 -10
  80. payi/types/limits/tag_update_params.py +0 -13
  81. payi/types/limits/tag_update_response.py +0 -10
  82. {payi-0.1.0a110.dist-info → payi-0.1.0a137.dist-info}/WHEEL +0 -0
  83. {payi-0.1.0a110.dist-info → payi-0.1.0a137.dist-info}/licenses/LICENSE +0 -0
@@ -1,18 +1,31 @@
1
+ from __future__ import annotations
2
+
1
3
  import os
2
4
  import json
3
- from typing import Any, Optional, Sequence
5
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence
4
6
  from functools import wraps
5
7
  from typing_extensions import override
6
8
 
7
9
  from wrapt import ObjectProxy, wrap_function_wrapper # type: ignore
8
10
 
9
- from payi.lib.helpers import PayiCategories, PayiHeaderNames, payi_aws_bedrock_url
11
+ from payi.lib.helpers import PayiCategories, PayiHeaderNames, PayiPropertyNames, payi_aws_bedrock_url
10
12
  from payi.types.ingest_units_params import Units
11
- from payi.types.pay_i_common_models_api_router_header_info_param import PayICommonModelsAPIRouterHeaderInfoParam
12
13
 
13
- from .instrument import _ChunkResult, _IsStreaming, _StreamingType, _ProviderRequest, _PayiInstrumentor
14
+ from .instrument import (
15
+ PayiInstrumentAwsBedrockConfig,
16
+ _Context,
17
+ _IsStreaming,
18
+ _PayiInstrumentor,
19
+ )
14
20
  from .version_helper import get_version_helper
21
+ from .ProviderRequest import _ChunkResult, _StreamingType, _ProviderRequest
22
+
23
+ if TYPE_CHECKING:
24
+ from tokenizers import Tokenizer # type: ignore
25
+ else:
26
+ Tokenizer = None
15
27
 
28
+ GUARDRAIL_SEMANTIC_FAILURE_DESCRIPTION = "Bedrock Guardrails intervened"
16
29
 
17
30
  class BedrockInstrumentor:
18
31
  _module_name: str = "boto3"
@@ -20,6 +33,37 @@ class BedrockInstrumentor:
20
33
 
21
34
  _instrumentor: _PayiInstrumentor
22
35
 
36
+ _guardrail_trace: bool = True
37
+
38
+ _model_mapping: Dict[str, _Context] = {}
39
+
40
+ _add_streaming_xproxy_result: bool = False
41
+
42
+ @staticmethod
43
+ def get_mapping(model_id: Optional[str]) -> _Context:
44
+ if not model_id:
45
+ return {}
46
+
47
+ return BedrockInstrumentor._model_mapping.get(model_id, {})
48
+
49
+ @staticmethod
50
+ def configure(aws_config: Optional[PayiInstrumentAwsBedrockConfig]) -> None:
51
+ if not aws_config:
52
+ return
53
+
54
+ trace = aws_config.get("guardrail_trace", True)
55
+ if trace is None:
56
+ trace = True
57
+ BedrockInstrumentor._guardrail_trace = trace
58
+
59
+ add_streaming_xproxy_result = aws_config.get("add_streaming_xproxy_result", False)
60
+ if add_streaming_xproxy_result:
61
+ BedrockInstrumentor._add_streaming_xproxy_result = add_streaming_xproxy_result
62
+
63
+ model_mappings = aws_config.get("model_mappings", [])
64
+ if model_mappings:
65
+ BedrockInstrumentor._model_mapping = _PayiInstrumentor._model_mapping_to_context_dict(model_mappings)
66
+
23
67
  @staticmethod
24
68
  def instrument(instrumentor: _PayiInstrumentor) -> None:
25
69
  BedrockInstrumentor._instrumentor = instrumentor
@@ -51,10 +95,10 @@ def create_client_wrapper(instrumentor: _PayiInstrumentor, wrapped: Any, instanc
51
95
 
52
96
  try:
53
97
  client: Any = wrapped(*args, **kwargs)
54
- client.invoke_model = wrap_invoke(instrumentor, client.invoke_model)
55
- client.invoke_model_with_response_stream = wrap_invoke_stream(instrumentor, client.invoke_model_with_response_stream)
56
- client.converse = wrap_converse(instrumentor, client.converse)
57
- client.converse_stream = wrap_converse_stream(instrumentor, client.converse_stream)
98
+ client.invoke_model = wrap_invoke(instrumentor, client.invoke_model, client)
99
+ client.invoke_model_with_response_stream = wrap_invoke_stream(instrumentor, client.invoke_model_with_response_stream, client)
100
+ client.converse = wrap_converse(instrumentor, client.converse, client)
101
+ client.converse_stream = wrap_converse_stream(instrumentor, client.converse_stream, client)
58
102
 
59
103
  instrumentor._logger.debug(f"Instrumented bedrock client")
60
104
 
@@ -100,17 +144,20 @@ def _redirect_to_payi(request: Any, event_name: str, **_: 'dict[str, Any]') -> N
100
144
  for key, value in extra_headers.items():
101
145
  request.headers[key] = value
102
146
 
103
-
104
147
  class InvokeResponseWrapper(ObjectProxy): # type: ignore
148
+ _cohere_embed_english_v3_tokenizer: Optional['Tokenizer'] = None
149
+
105
150
  def __init__(
106
151
  self,
107
- response: Any,
152
+ response: 'dict[str, Any]',
153
+ body: Any,
108
154
  request: '_BedrockInvokeProviderRequest',
109
155
  log_prompt_and_response: bool
110
156
  ) -> None:
111
157
 
112
- super().__init__(response) # type: ignore
158
+ super().__init__(body) # type: ignore
113
159
  self._response = response
160
+ self._body = body
114
161
  self._request = request
115
162
  self._log_prompt_and_response = log_prompt_and_response
116
163
 
@@ -160,14 +207,50 @@ class InvokeResponseWrapper(ObjectProxy): # type: ignore
160
207
 
161
208
  bedrock_converse_process_synchronous_function_call(self._request, response)
162
209
 
210
+ elif self._request._is_amazon_titan_embed_text_v1:
211
+ input = response.get('inputTextTokenCount', 0)
212
+ units["text"] = Units(input=input, output=0)
213
+
214
+ elif self._request._is_cohere_embed_english_v3:
215
+ texts: list[str] = response.get("texts", [])
216
+ if texts and len(texts) > 0:
217
+ text = " ".join(texts)
218
+
219
+ try:
220
+ from tokenizers import Tokenizer # type: ignore
221
+
222
+ if self._cohere_embed_english_v3_tokenizer is None: # type: ignore
223
+ current_dir = os.path.dirname(os.path.abspath(__file__))
224
+ tokenizer_path = os.path.join(current_dir, "data", "cohere_embed_english_v3.json")
225
+ self._cohere_embed_english_v3_tokenizer = Tokenizer.from_file(tokenizer_path) # type: ignore
226
+
227
+ if self._cohere_embed_english_v3_tokenizer is not None and isinstance(self._cohere_embed_english_v3_tokenizer, Tokenizer): # type: ignore
228
+ tokens: list = self._cohere_embed_english_v3_tokenizer.encode(text, add_special_tokens=False).tokens # type: ignore
229
+
230
+ if tokens and isinstance(tokens, list):
231
+ units["text"] = Units(input=len(tokens), output=0) # type: ignore
232
+
233
+ except ImportError:
234
+ self._request._instrumentor._logger.warning("tokenizers module not found, caller must install the tokenizers module. Cannot record text tokens for Cohere embed english v3")
235
+ pass
236
+ except Exception as e:
237
+ self._request._instrumentor._logger.warning(f"Error processing Cohere embed english v3 response: {e}")
238
+ pass
239
+
163
240
  if self._log_prompt_and_response:
164
241
  ingest["provider_response_json"] = data.decode('utf-8') # type: ignore
165
-
166
- self._request._instrumentor._ingest_units(self._request)
242
+
243
+ guardrails = response.get("amazon-bedrock-trace", {}).get("guardrail", {}).get("input", {})
244
+ self._request.process_guardrails(guardrails)
245
+
246
+ self._request.process_stop_action(response.get("amazon-bedrock-guardrailAction", ""))
247
+
248
+ xproxy_result = self._request._instrumentor._ingest_units(self._request)
249
+ self._request.assign_xproxy_result(self._response, xproxy_result)
167
250
 
168
251
  return data # type: ignore
169
252
 
170
- def wrap_invoke(instrumentor: _PayiInstrumentor, wrapped: Any) -> Any:
253
+ def wrap_invoke(instrumentor: _PayiInstrumentor, wrapped: Any, instance: Any) -> Any:
171
254
  @wraps(wrapped)
172
255
  def invoke_wrapper(*args: Any, **kwargs: 'dict[str, Any]') -> Any:
173
256
  modelId:str = kwargs.get("modelId", "") # type: ignore
@@ -176,14 +259,14 @@ def wrap_invoke(instrumentor: _PayiInstrumentor, wrapped: Any) -> Any:
176
259
  _BedrockInvokeProviderRequest(instrumentor=instrumentor, model_id=modelId),
177
260
  _IsStreaming.false,
178
261
  wrapped,
179
- None,
262
+ instance,
180
263
  args,
181
264
  kwargs,
182
265
  )
183
266
 
184
267
  return invoke_wrapper
185
268
 
186
- def wrap_invoke_stream(instrumentor: _PayiInstrumentor, wrapped: Any) -> Any:
269
+ def wrap_invoke_stream(instrumentor: _PayiInstrumentor, wrapped: Any, instance: Any) -> Any:
187
270
  @wraps(wrapped)
188
271
  def invoke_wrapper(*args: Any, **kwargs: Any) -> Any:
189
272
  modelId: str = kwargs.get("modelId", "") # type: ignore
@@ -193,14 +276,14 @@ def wrap_invoke_stream(instrumentor: _PayiInstrumentor, wrapped: Any) -> Any:
193
276
  _BedrockInvokeProviderRequest(instrumentor=instrumentor, model_id=modelId),
194
277
  _IsStreaming.true,
195
278
  wrapped,
196
- None,
279
+ instance,
197
280
  args,
198
281
  kwargs,
199
282
  )
200
283
 
201
284
  return invoke_wrapper
202
285
 
203
- def wrap_converse(instrumentor: _PayiInstrumentor, wrapped: Any) -> Any:
286
+ def wrap_converse(instrumentor: _PayiInstrumentor, wrapped: Any, instance: Any) -> Any:
204
287
  @wraps(wrapped)
205
288
  def invoke_wrapper(*args: Any, **kwargs: 'dict[str, Any]') -> Any:
206
289
  modelId:str = kwargs.get("modelId", "") # type: ignore
@@ -210,14 +293,14 @@ def wrap_converse(instrumentor: _PayiInstrumentor, wrapped: Any) -> Any:
210
293
  _BedrockConverseProviderRequest(instrumentor=instrumentor),
211
294
  _IsStreaming.false,
212
295
  wrapped,
213
- None,
296
+ instance,
214
297
  args,
215
298
  kwargs,
216
299
  )
217
300
 
218
301
  return invoke_wrapper
219
302
 
220
- def wrap_converse_stream(instrumentor: _PayiInstrumentor, wrapped: Any) -> Any:
303
+ def wrap_converse_stream(instrumentor: _PayiInstrumentor, wrapped: Any, instance: Any) -> Any:
221
304
  @wraps(wrapped)
222
305
  def invoke_wrapper(*args: Any, **kwargs: Any) -> Any:
223
306
  modelId: str = kwargs.get("modelId", "") # type: ignore
@@ -227,7 +310,7 @@ def wrap_converse_stream(instrumentor: _PayiInstrumentor, wrapped: Any) -> Any:
227
310
  _BedrockConverseProviderRequest(instrumentor=instrumentor),
228
311
  _IsStreaming.true,
229
312
  wrapped,
230
- None,
313
+ instance,
231
314
  args,
232
315
  kwargs,
233
316
  )
@@ -235,6 +318,7 @@ def wrap_converse_stream(instrumentor: _PayiInstrumentor, wrapped: Any) -> Any:
235
318
  return invoke_wrapper
236
319
 
237
320
  class _BedrockProviderRequest(_ProviderRequest):
321
+
238
322
  def __init__(self, instrumentor: _PayiInstrumentor):
239
323
  super().__init__(
240
324
  instrumentor=instrumentor,
@@ -246,15 +330,40 @@ class _BedrockProviderRequest(_ProviderRequest):
246
330
  )
247
331
 
248
332
  @override
249
- def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
250
- # boto3 doesn't allow extra_headers
251
- kwargs.pop("extra_headers", None)
252
- self._ingest["resource"] = kwargs.get("modelId", "")
333
+ def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
334
+ modelId = kwargs.get("modelId", "")
335
+ self._ingest["resource"] = modelId
336
+
337
+ if not self._price_as.resource and not self._price_as.category and BedrockInstrumentor._model_mapping:
338
+ deployment = BedrockInstrumentor._model_mapping.get(modelId, {})
339
+ self._price_as.category = deployment.get("price_as_category", "")
340
+ self._price_as.resource = deployment.get("price_as_resource", "")
341
+ self._price_as.resource_scope = deployment.get("resource_scope", None)
342
+
343
+ if self._price_as.resource_scope:
344
+ self._ingest["resource_scope"] = self._price_as.resource_scope
345
+
346
+ # override defaults
347
+ if self._price_as.category:
348
+ self._ingest["category"] = self._price_as.category
349
+ if self._price_as.resource:
350
+ self._ingest["resource"] = self._price_as.resource
351
+
253
352
  return True
254
353
 
354
+ def process_response_metadata(self, metadata: 'dict[str, Any]') -> None:
355
+ request_id = metadata.get("RequestId", "")
356
+ if request_id:
357
+ self._ingest["provider_response_id"] = request_id
358
+
359
+ response_headers = metadata.get("HTTPHeaders", {})
360
+ if response_headers:
361
+ self.add_response_headers(response_headers)
362
+
255
363
  @override
256
364
  def process_initial_stream_response(self, response: Any) -> None:
257
- self._ingest["provider_response_id"] = response.get("ResponseMetadata", {}).get("RequestId", None)
365
+ super().process_initial_stream_response(response)
366
+ self.process_response_metadata(response.get("ResponseMetadata", {}))
258
367
 
259
368
  @override
260
369
  def process_exception(self, exception: Exception, kwargs: Any, ) -> bool:
@@ -281,12 +390,73 @@ class _BedrockProviderRequest(_ProviderRequest):
281
390
  self._instrumentor._logger.debug(f"Error processing exception: {e}")
282
391
  return False
283
392
 
393
+ def process_guardrails(self, guardrails: 'dict[str, Any]') -> None:
394
+ units = self._ingest["units"]
395
+
396
+ # while we iterate over the entire dict, only one guardrail is expected and supported
397
+ for _, value in guardrails.items():
398
+ # _ (key) is the guardrail id
399
+ if not isinstance(value, dict):
400
+ continue
401
+
402
+ usage: dict[str, int] = value.get("invocationMetrics", {}).get("usage", {}) # type: ignore
403
+ if not usage:
404
+ continue
405
+
406
+ topicPolicyUnits: int = usage.get("topicPolicyUnits", 0) # type: ignore
407
+ if topicPolicyUnits > 0:
408
+ units["guardrail_topic"] = Units(input=topicPolicyUnits, output=0) # type: ignore
409
+
410
+ contentPolicyUnits = usage.get("contentPolicyUnits", 0) # type: ignore
411
+ if contentPolicyUnits > 0:
412
+ units["guardrail_content"] = Units(input=contentPolicyUnits, output=0) # type: ignore
413
+
414
+ wordPolicyUnits = usage.get("wordPolicyUnits", 0) # type: ignore
415
+ if wordPolicyUnits > 0:
416
+ units["guardrail_word_free"] = Units(input=wordPolicyUnits, output=0) # type: ignore
417
+
418
+ automatedReasoningPolicyUnits = usage.get("automatedReasoningPolicyUnits", 0) # type: ignore
419
+ if automatedReasoningPolicyUnits > 0:
420
+ units["guardrail_automated_reasoning"] = Units(input=automatedReasoningPolicyUnits, output=0) # type: ignore
421
+
422
+ sensitiveInformationPolicyUnits = usage.get("sensitiveInformationPolicyUnits", 0) # type: ignore
423
+ if sensitiveInformationPolicyUnits > 0:
424
+ units["guardrail_sensitive_information"] = Units(input=sensitiveInformationPolicyUnits, output=0) # type: ignore
425
+
426
+ sensitiveInformationPolicyFreeUnits = usage.get("sensitiveInformationPolicyFreeUnits", 0) # type: ignore
427
+ if sensitiveInformationPolicyFreeUnits > 0:
428
+ units["guardrail_sensitive_information_free"] = Units(input=sensitiveInformationPolicyFreeUnits, output=0) # type: ignore
429
+
430
+ contextualGroundingPolicyUnits = usage.get("contextualGroundingPolicyUnits", 0) # type: ignore
431
+ if contextualGroundingPolicyUnits > 0:
432
+ units["guardrail_contextual_grounding"] = Units(input=contextualGroundingPolicyUnits, output=0) # type: ignore
433
+
434
+ contentPolicyImageUnits = usage.get("contentPolicyImageUnits", 0) # type: ignore
435
+ if contentPolicyImageUnits > 0:
436
+ units["guardrail_content_image"] = Units(input=contentPolicyImageUnits, output=0) # type: ignore
437
+
284
438
  class _BedrockInvokeProviderRequest(_BedrockProviderRequest):
285
439
  def __init__(self, instrumentor: _PayiInstrumentor, model_id: str):
286
440
  super().__init__(instrumentor=instrumentor)
287
- self._is_anthropic: bool = 'anthropic' in model_id
288
- self._is_nova: bool = 'nova' in model_id
289
- self._is_meta: bool = 'meta' in model_id
441
+
442
+ price_as_resource = BedrockInstrumentor._model_mapping.get(model_id, {}).get("price_as_resource", None)
443
+ if price_as_resource:
444
+ model_id = price_as_resource
445
+
446
+ self._is_anthropic: bool = False
447
+ self._is_nova: bool = False
448
+ self._is_meta: bool = False
449
+ self._is_amazon_titan_embed_text_v1: bool = False
450
+ self._is_cohere_embed_english_v3: bool = False
451
+
452
+ self._assign_model_state(model_id=model_id)
453
+
454
+ def _assign_model_state(self, model_id: str) -> None:
455
+ self._is_anthropic = 'anthropic' in model_id
456
+ self._is_nova = 'nova' in model_id
457
+ self._is_meta = 'meta' in model_id
458
+ self._is_amazon_titan_embed_text_v1 = 'amazon.titan-embed-text-v1' == model_id
459
+ self._is_cohere_embed_english_v3 = 'cohere.embed-english-v3' == model_id
290
460
 
291
461
  @override
292
462
  def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
@@ -294,21 +464,54 @@ class _BedrockInvokeProviderRequest(_BedrockProviderRequest):
294
464
 
295
465
  super().process_request(instance, extra_headers, args, kwargs)
296
466
 
467
+ # super().process_request will assign price_as mapping from global state, so evaluate afterwards
468
+ if self._price_as.resource:
469
+ self._assign_model_state(model_id=self._price_as.resource)
470
+
471
+ guardrail_id = kwargs.get("guardrailIdentifier", "")
472
+ if guardrail_id:
473
+ self.add_internal_request_property(PayiPropertyNames.aws_bedrock_guardrail_id, guardrail_id)
474
+
475
+ guardrail_version = kwargs.get("guardrailVersion", "")
476
+ if guardrail_version:
477
+ self.add_internal_request_property(PayiPropertyNames.aws_bedrock_guardrail_version, guardrail_version)
478
+
479
+ if guardrail_id and guardrail_version and BedrockInstrumentor._guardrail_trace:
480
+ trace = kwargs.get("trace", None)
481
+ if not trace:
482
+ kwargs["trace"] = "ENABLED"
483
+
297
484
  if self._is_anthropic:
298
485
  try:
299
- body = json.loads( kwargs.get("body", ""))
486
+ body = json.loads(kwargs.get("body", ""))
300
487
  messages = body.get("messages", {})
301
488
  if messages:
302
489
  anthropic_has_image_and_get_texts(self, messages)
303
490
  except Exception as e:
304
491
  self._instrumentor._logger.debug(f"Bedrock invoke error processing request body: {e}")
305
-
492
+ elif self._is_cohere_embed_english_v3:
493
+ try:
494
+ body = json.loads(kwargs.get("body", ""))
495
+ input_type = body.get("input_type", "")
496
+ if input_type == 'image':
497
+ images = body.get("images", [])
498
+ if (len(images) > 0):
499
+ # only supports one image according to docs
500
+ self._ingest["units"]["vision"] = Units(input=1, output=0)
501
+ except Exception as e:
502
+ self._instrumentor._logger.debug(f"Bedrock invoke error processing request body: {e}")
306
503
  return True
307
504
 
308
505
  @override
309
506
  def process_chunk(self, chunk: Any) -> _ChunkResult:
310
507
  chunk_dict = json.loads(chunk)
311
508
 
509
+ guardrails = chunk_dict.get("amazon-bedrock-trace", {}).get("guardrail", {}).get("input", {})
510
+ if guardrails:
511
+ self.process_guardrails(guardrails)
512
+
513
+ self.process_stop_action(chunk_dict.get("amazon-bedrock-guardrailAction", ""))
514
+
312
515
  if self._is_anthropic:
313
516
  from .AnthropicInstrumentor import anthropic_process_chunk
314
517
  return anthropic_process_chunk(self, chunk_dict, assign_id=False)
@@ -347,23 +550,23 @@ class _BedrockInvokeProviderRequest(_BedrockProviderRequest):
347
550
  log_prompt_and_response: bool,
348
551
  kwargs: Any) -> Any:
349
552
 
350
- metadata = response.get("ResponseMetadata", {})
351
-
352
- request_id = metadata.get("RequestId", "")
353
- if request_id:
354
- self._ingest["provider_response_id"] = request_id
355
-
356
- response_headers = metadata.get("HTTPHeaders", {}).copy()
357
- if response_headers:
358
- self._ingest["provider_response_headers"] = [PayICommonModelsAPIRouterHeaderInfoParam(name=k, value=v) for k, v in response_headers.items()]
553
+ self.process_response_metadata(response.get("ResponseMetadata", {}))
359
554
 
360
555
  response["body"] = InvokeResponseWrapper(
361
- response=response["body"],
556
+ response=response,
557
+ body=response["body"],
362
558
  request=self,
363
559
  log_prompt_and_response=log_prompt_and_response)
364
560
 
365
561
  return response
366
562
 
563
+ def process_stop_action(self, action: str) -> None:
564
+ # record both as a semantic failure and guardrail action so it is discoverable through both properties
565
+ if action == "INTERVENED":
566
+ self.add_internal_request_property(PayiPropertyNames.failure, action)
567
+ self.add_internal_request_property(PayiPropertyNames.failure_description, GUARDRAIL_SEMANTIC_FAILURE_DESCRIPTION)
568
+ self.add_internal_request_property(PayiPropertyNames.aws_bedrock_guardrail_action, action)
569
+
367
570
  @override
368
571
  def remove_inline_data(self, prompt: 'dict[str, Any]') -> bool:# noqa: ARG002
369
572
  if not self._is_anthropic:
@@ -383,6 +586,27 @@ class _BedrockInvokeProviderRequest(_BedrockProviderRequest):
383
586
  return False
384
587
 
385
588
  class _BedrockConverseProviderRequest(_BedrockProviderRequest):
589
+ @override
590
+ def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
591
+ super().process_request(instance, extra_headers, args, kwargs)
592
+
593
+ guardrail_config = kwargs.get("guardrailConfig", {})
594
+ if guardrail_config:
595
+ guardrailIdentifier = guardrail_config.get("guardrailIdentifier", "")
596
+ if guardrailIdentifier:
597
+ self.add_internal_request_property(PayiPropertyNames.aws_bedrock_guardrail_id, guardrailIdentifier)
598
+
599
+ guardrailVersion = guardrail_config.get("guardrailVersion", "")
600
+ if guardrailVersion:
601
+ self.add_internal_request_property(PayiPropertyNames.aws_bedrock_guardrail_version, guardrailVersion)
602
+
603
+ if guardrailIdentifier and guardrailVersion and BedrockInstrumentor._guardrail_trace:
604
+ trace = guardrail_config.get("trace", None)
605
+ if not trace:
606
+ guardrail_config["trace"] = "enabled"
607
+
608
+ return True
609
+
386
610
  @override
387
611
  def process_synchronous_response(
388
612
  self,
@@ -390,22 +614,14 @@ class _BedrockConverseProviderRequest(_BedrockProviderRequest):
390
614
  log_prompt_and_response: bool,
391
615
  kwargs: Any) -> Any:
392
616
 
393
- usage = response["usage"]
394
- input = usage["inputTokens"]
395
- output = usage["outputTokens"]
396
-
617
+ usage = response.get("usage", {})
618
+ input = usage.get("inputTokens", 0)
619
+ output = usage.get("outputTokens", 0)
620
+
397
621
  units: dict[str, Units] = self._ingest["units"]
398
622
  units["text"] = Units(input=input, output=output)
399
623
 
400
- metadata = response.get("ResponseMetadata", {})
401
-
402
- request_id = metadata.get("RequestId", "")
403
- if request_id:
404
- self._ingest["provider_response_id"] = request_id
405
-
406
- response_headers = metadata.get("HTTPHeaders", {})
407
- if response_headers:
408
- self._ingest["provider_response_headers"] = [PayICommonModelsAPIRouterHeaderInfoParam(name=k, value=v) for k, v in response_headers.items()]
624
+ self.process_response_metadata(response.get("ResponseMetadata", {}))
409
625
 
410
626
  if log_prompt_and_response:
411
627
  response_without_metadata = response.copy()
@@ -414,6 +630,12 @@ class _BedrockConverseProviderRequest(_BedrockProviderRequest):
414
630
 
415
631
  bedrock_converse_process_synchronous_function_call(self, response)
416
632
 
633
+ guardrails = response.get("trace", {}).get("guardrail", {}).get("inputAssessment", {})
634
+ if guardrails:
635
+ self.process_guardrails(guardrails)
636
+
637
+ self.process_stop_reason(response.get("stopReason", ""))
638
+
417
639
  return None
418
640
 
419
641
  @override
@@ -422,17 +644,30 @@ class _BedrockConverseProviderRequest(_BedrockProviderRequest):
422
644
  metadata = chunk.get("metadata", {})
423
645
 
424
646
  if metadata:
425
- usage = metadata['usage']
426
- input = usage["inputTokens"]
427
- output = usage["outputTokens"]
647
+ usage = metadata.get('usage', {})
648
+ input = usage.get("inputTokens", 0)
649
+ output = usage.get("outputTokens", 0)
428
650
  self._ingest["units"]["text"] = Units(input=input, output=output)
429
651
 
652
+ guardrail = metadata.get("trace", {}).get("guardrail", {}).get("inputAssessment", {})
653
+ if guardrail:
654
+ self.process_guardrails(guardrail)
655
+
430
656
  ingest = True
431
657
 
658
+ self.process_stop_reason(chunk.get("messageStop", {}).get("stopReason", ""))
659
+
432
660
  bedrock_converse_process_streaming_for_function_call(self, chunk)
433
661
 
434
662
  return _ChunkResult(send_chunk_to_caller=True, ingest=ingest)
435
663
 
664
+ def process_stop_reason(self, reason: str) -> None:
665
+ if reason == "guardrail_intervened":
666
+ # record both as a semantic failure and guardrail action so it is discoverable through both properties
667
+ self.add_internal_request_property(PayiPropertyNames.failure, reason)
668
+ self.add_internal_request_property(PayiPropertyNames.failure_description, GUARDRAIL_SEMANTIC_FAILURE_DESCRIPTION)
669
+ self.add_internal_request_property(PayiPropertyNames.aws_bedrock_guardrail_action, reason)
670
+
436
671
  def bedrock_converse_process_streaming_for_function_call(request: _ProviderRequest, chunk: 'dict[str, Any]') -> None:
437
672
  contentBlockStart = chunk.get("contentBlockStart", {})
438
673
  tool_use = contentBlockStart.get("start", {}).get("toolUse", {})
@@ -1,11 +1,14 @@
1
+ from __future__ import annotations
2
+
1
3
  from typing import Any, List, Union, Sequence
2
4
  from typing_extensions import override
3
5
 
4
6
  from wrapt import wrap_function_wrapper # type: ignore
5
7
 
6
- from .instrument import _ChunkResult, _IsStreaming, _PayiInstrumentor
8
+ from .instrument import _IsStreaming, _PayiInstrumentor
7
9
  from .VertexRequest import _VertexRequest
8
10
  from .version_helper import get_version_helper
11
+ from .ProviderRequest import _ChunkResult
9
12
 
10
13
 
11
14
  class GoogleGenAiInstrumentor:
@@ -14,36 +17,20 @@ class GoogleGenAiInstrumentor:
14
17
 
15
18
  @staticmethod
16
19
  def instrument(instrumentor: _PayiInstrumentor) -> None:
17
- try:
18
- GoogleGenAiInstrumentor._module_version = get_version_helper(GoogleGenAiInstrumentor._module_name)
19
-
20
- wrap_function_wrapper(
21
- "google.genai.models",
22
- "Models.generate_content",
23
- generate_wrapper(instrumentor),
24
- )
25
-
26
- wrap_function_wrapper(
27
- "google.genai.models",
28
- "Models.generate_content_stream",
29
- generate_stream_wrapper(instrumentor),
30
- )
31
-
32
- wrap_function_wrapper(
33
- "google.genai.models",
34
- "AsyncModels.generate_content",
35
- agenerate_wrapper(instrumentor),
36
- )
37
-
38
- wrap_function_wrapper(
39
- "google.genai.models",
40
- "AsyncModels.generate_content_stream",
41
- agenerate_stream_wrapper(instrumentor),
42
- )
43
-
44
- except Exception as e:
45
- instrumentor._logger.debug(f"Error instrumenting vertex: {e}")
46
- return
20
+ GoogleGenAiInstrumentor._module_version = get_version_helper(GoogleGenAiInstrumentor._module_name)
21
+
22
+ wrappers = [
23
+ ("google.genai.models", "Models.generate_content", generate_wrapper(instrumentor)),
24
+ ("google.genai.models", "Models.generate_content_stream", generate_stream_wrapper(instrumentor)),
25
+ ("google.genai.models", "AsyncModels.generate_content", agenerate_wrapper(instrumentor)),
26
+ ("google.genai.models", "AsyncModels.generate_content_stream", agenerate_stream_wrapper(instrumentor)),
27
+ ]
28
+
29
+ for module, method, wrapper in wrappers:
30
+ try:
31
+ wrap_function_wrapper(module, method, wrapper)
32
+ except Exception as e:
33
+ instrumentor._logger.debug(f"Error wrapping {module}.{method}: {e}")
47
34
 
48
35
  @_PayiInstrumentor.payi_wrapper
49
36
  def generate_wrapper(