payi 0.1.0a133__py3-none-any.whl → 0.1.0a134__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.

Potentially problematic release.


This version of payi might be problematic. Click here for more details.

payi/_version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  __title__ = "payi"
4
- __version__ = "0.1.0-alpha.133" # x-release-please-version
4
+ __version__ = "0.1.0-alpha.134" # x-release-please-version
@@ -195,15 +195,26 @@ class _AnthropicProviderRequest(_ProviderRequest):
195
195
 
196
196
  return None
197
197
 
198
+ def _update_resource_name(self, model: str) -> str:
199
+ return ("anthropic." if self._is_vertex else "") + model
200
+
198
201
  @override
199
- def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
200
- self._ingest["resource"] = ("anthropic." if self._is_vertex else "") + kwargs.get("model", "")
202
+ def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
203
+ self._ingest["resource"] = self._update_resource_name(kwargs.get("model", ""))
204
+
205
+ if self._price_as.resource_scope:
206
+ self._ingest["resource_scope"] = self._price_as.resource_scope
207
+
208
+ # override defaults
209
+ if self._price_as.category:
210
+ self._ingest["category"] = self._price_as.category
211
+ if self._price_as.resource:
212
+ self._ingest["resource"] = self._update_resource_name(self._price_as.resource)
201
213
 
202
214
  self._instrumentor._logger.debug(f"Processing anthropic request: model {self._ingest['resource']}, category {self._category}")
203
215
 
204
216
  messages = kwargs.get("messages")
205
217
  if messages:
206
-
207
218
  anthropic_has_image_and_get_texts(self, messages)
208
219
 
209
220
  return True
@@ -1,6 +1,6 @@
1
1
  import os
2
2
  import json
3
- from typing import TYPE_CHECKING, Any, Optional, Sequence
3
+ from typing import TYPE_CHECKING, Any, Dict, Optional, Sequence
4
4
  from functools import wraps
5
5
  from typing_extensions import override
6
6
 
@@ -12,6 +12,7 @@ from payi.types.pay_i_common_models_api_router_header_info_param import PayIComm
12
12
 
13
13
  from .instrument import (
14
14
  PayiInstrumentAwsBedrockConfig,
15
+ _Context,
15
16
  _ChunkResult,
16
17
  _IsStreaming,
17
18
  _StreamingType,
@@ -35,8 +36,31 @@ class BedrockInstrumentor:
35
36
 
36
37
  _guardrail_trace: bool = True
37
38
 
39
+ _model_mapping: Dict[str, _Context] = {}
40
+
41
+ @staticmethod
42
+ def get_mapping(model_id: Optional[str]) -> _Context:
43
+ if not model_id:
44
+ return {}
45
+
46
+ return BedrockInstrumentor._model_mapping.get(model_id, {})
47
+
48
+ @staticmethod
49
+ def configure(aws_config: Optional[PayiInstrumentAwsBedrockConfig]) -> None:
50
+ if not aws_config:
51
+ return
52
+
53
+ trace = aws_config.get("guardrail_trace", True)
54
+ if trace is None:
55
+ trace = True
56
+ BedrockInstrumentor._guardrail_trace = trace
57
+
58
+ model_mappings = aws_config.get("model_mappings", [])
59
+ if model_mappings:
60
+ BedrockInstrumentor._model_mapping = _PayiInstrumentor._model_mapping_to_context_dict(model_mappings)
61
+
38
62
  @staticmethod
39
- def instrument(instrumentor: _PayiInstrumentor, aws_config: Optional[PayiInstrumentAwsBedrockConfig]) -> None:
63
+ def instrument(instrumentor: _PayiInstrumentor) -> None:
40
64
  BedrockInstrumentor._instrumentor = instrumentor
41
65
 
42
66
  BedrockInstrumentor._module_version = get_version_helper(BedrockInstrumentor._module_name)
@@ -58,9 +82,6 @@ class BedrockInstrumentor:
58
82
  instrumentor._logger.debug(f"Error instrumenting bedrock: {e}")
59
83
  return
60
84
 
61
- if aws_config:
62
- BedrockInstrumentor._guardrail_trace = aws_config.get("guardrail_trace", True)
63
-
64
85
  @_PayiInstrumentor.payi_wrapper
65
86
  def create_client_wrapper(instrumentor: _PayiInstrumentor, wrapped: Any, instance: Any, *args: Any, **kwargs: Any) -> Any: # noqa: ARG001
66
87
  if kwargs.get("service_name") != "bedrock-runtime":
@@ -69,10 +90,10 @@ def create_client_wrapper(instrumentor: _PayiInstrumentor, wrapped: Any, instanc
69
90
 
70
91
  try:
71
92
  client: Any = wrapped(*args, **kwargs)
72
- client.invoke_model = wrap_invoke(instrumentor, client.invoke_model)
73
- client.invoke_model_with_response_stream = wrap_invoke_stream(instrumentor, client.invoke_model_with_response_stream)
74
- client.converse = wrap_converse(instrumentor, client.converse)
75
- client.converse_stream = wrap_converse_stream(instrumentor, client.converse_stream)
93
+ client.invoke_model = wrap_invoke(instrumentor, client.invoke_model, client)
94
+ client.invoke_model_with_response_stream = wrap_invoke_stream(instrumentor, client.invoke_model_with_response_stream, client)
95
+ client.converse = wrap_converse(instrumentor, client.converse, client)
96
+ client.converse_stream = wrap_converse_stream(instrumentor, client.converse_stream, client)
76
97
 
77
98
  instrumentor._logger.debug(f"Instrumented bedrock client")
78
99
 
@@ -221,7 +242,7 @@ class InvokeResponseWrapper(ObjectProxy): # type: ignore
221
242
 
222
243
  return data # type: ignore
223
244
 
224
- def wrap_invoke(instrumentor: _PayiInstrumentor, wrapped: Any) -> Any:
245
+ def wrap_invoke(instrumentor: _PayiInstrumentor, wrapped: Any, instance: Any) -> Any:
225
246
  @wraps(wrapped)
226
247
  def invoke_wrapper(*args: Any, **kwargs: 'dict[str, Any]') -> Any:
227
248
  modelId:str = kwargs.get("modelId", "") # type: ignore
@@ -230,14 +251,14 @@ def wrap_invoke(instrumentor: _PayiInstrumentor, wrapped: Any) -> Any:
230
251
  _BedrockInvokeProviderRequest(instrumentor=instrumentor, model_id=modelId),
231
252
  _IsStreaming.false,
232
253
  wrapped,
233
- None,
254
+ instance,
234
255
  args,
235
256
  kwargs,
236
257
  )
237
258
 
238
259
  return invoke_wrapper
239
260
 
240
- def wrap_invoke_stream(instrumentor: _PayiInstrumentor, wrapped: Any) -> Any:
261
+ def wrap_invoke_stream(instrumentor: _PayiInstrumentor, wrapped: Any, instance: Any) -> Any:
241
262
  @wraps(wrapped)
242
263
  def invoke_wrapper(*args: Any, **kwargs: Any) -> Any:
243
264
  modelId: str = kwargs.get("modelId", "") # type: ignore
@@ -247,14 +268,14 @@ def wrap_invoke_stream(instrumentor: _PayiInstrumentor, wrapped: Any) -> Any:
247
268
  _BedrockInvokeProviderRequest(instrumentor=instrumentor, model_id=modelId),
248
269
  _IsStreaming.true,
249
270
  wrapped,
250
- None,
271
+ instance,
251
272
  args,
252
273
  kwargs,
253
274
  )
254
275
 
255
276
  return invoke_wrapper
256
277
 
257
- def wrap_converse(instrumentor: _PayiInstrumentor, wrapped: Any) -> Any:
278
+ def wrap_converse(instrumentor: _PayiInstrumentor, wrapped: Any, instance: Any) -> Any:
258
279
  @wraps(wrapped)
259
280
  def invoke_wrapper(*args: Any, **kwargs: 'dict[str, Any]') -> Any:
260
281
  modelId:str = kwargs.get("modelId", "") # type: ignore
@@ -264,14 +285,14 @@ def wrap_converse(instrumentor: _PayiInstrumentor, wrapped: Any) -> Any:
264
285
  _BedrockConverseProviderRequest(instrumentor=instrumentor),
265
286
  _IsStreaming.false,
266
287
  wrapped,
267
- None,
288
+ instance,
268
289
  args,
269
290
  kwargs,
270
291
  )
271
292
 
272
293
  return invoke_wrapper
273
294
 
274
- def wrap_converse_stream(instrumentor: _PayiInstrumentor, wrapped: Any) -> Any:
295
+ def wrap_converse_stream(instrumentor: _PayiInstrumentor, wrapped: Any, instance: Any) -> Any:
275
296
  @wraps(wrapped)
276
297
  def invoke_wrapper(*args: Any, **kwargs: Any) -> Any:
277
298
  modelId: str = kwargs.get("modelId", "") # type: ignore
@@ -281,7 +302,7 @@ def wrap_converse_stream(instrumentor: _PayiInstrumentor, wrapped: Any) -> Any:
281
302
  _BedrockConverseProviderRequest(instrumentor=instrumentor),
282
303
  _IsStreaming.true,
283
304
  wrapped,
284
- None,
305
+ instance,
285
306
  args,
286
307
  kwargs,
287
308
  )
@@ -301,10 +322,25 @@ class _BedrockProviderRequest(_ProviderRequest):
301
322
  )
302
323
 
303
324
  @override
304
- def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
305
- # boto3 doesn't allow extra_headers
306
- kwargs.pop("extra_headers", None)
307
- self._ingest["resource"] = kwargs.get("modelId", "")
325
+ def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
326
+ modelId = kwargs.get("modelId", "")
327
+ self._ingest["resource"] = modelId
328
+
329
+ if not self._price_as.resource and not self._price_as.category and BedrockInstrumentor._model_mapping:
330
+ deployment = BedrockInstrumentor._model_mapping.get(modelId, {})
331
+ self._price_as.category = deployment.get("price_as_category", "")
332
+ self._price_as.resource = deployment.get("price_as_resource", "")
333
+ self._price_as.resource_scope = deployment.get("resource_scope", None)
334
+
335
+ if self._price_as.resource_scope:
336
+ self._ingest["resource_scope"] = self._price_as.resource_scope
337
+
338
+ # override defaults
339
+ if self._price_as.category:
340
+ self._ingest["category"] = self._price_as.category
341
+ if self._price_as.resource:
342
+ self._ingest["resource"] = self._price_as.resource
343
+
308
344
  return True
309
345
 
310
346
  @override
@@ -384,11 +420,25 @@ class _BedrockProviderRequest(_ProviderRequest):
384
420
  class _BedrockInvokeProviderRequest(_BedrockProviderRequest):
385
421
  def __init__(self, instrumentor: _PayiInstrumentor, model_id: str):
386
422
  super().__init__(instrumentor=instrumentor)
387
- self._is_anthropic: bool = 'anthropic' in model_id
388
- self._is_nova: bool = 'nova' in model_id
389
- self._is_meta: bool = 'meta' in model_id
390
- self._is_amazon_titan_embed_text_v1: bool = 'amazon.titan-embed-text-v1' == model_id
391
- self._is_cohere_embed_english_v3: bool = 'cohere.embed-english-v3' == model_id
423
+
424
+ price_as_resource = BedrockInstrumentor._model_mapping.get(model_id, {}).get("price_as_resource", None)
425
+ if price_as_resource:
426
+ model_id = price_as_resource
427
+
428
+ self._is_anthropic: bool = False
429
+ self._is_nova: bool = False
430
+ self._is_meta: bool = False
431
+ self._is_amazon_titan_embed_text_v1: bool = False
432
+ self._is_cohere_embed_english_v3: bool = False
433
+
434
+ self._assign_model_state(model_id=model_id)
435
+
436
+ def _assign_model_state(self, model_id: str) -> None:
437
+ self._is_anthropic = 'anthropic' in model_id
438
+ self._is_nova = 'nova' in model_id
439
+ self._is_meta = 'meta' in model_id
440
+ self._is_amazon_titan_embed_text_v1 = 'amazon.titan-embed-text-v1' == model_id
441
+ self._is_cohere_embed_english_v3 = 'cohere.embed-english-v3' == model_id
392
442
 
393
443
  @override
394
444
  def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
@@ -396,6 +446,10 @@ class _BedrockInvokeProviderRequest(_BedrockProviderRequest):
396
446
 
397
447
  super().process_request(instance, extra_headers, args, kwargs)
398
448
 
449
+ # super().process_request will assign price_as mapping from global state, so evaluate afterwards
450
+ if self._price_as.resource:
451
+ self._assign_model_state(model_id=self._price_as.resource)
452
+
399
453
  guardrail_id = kwargs.get("guardrailIdentifier", "")
400
454
  if guardrail_id:
401
455
  self.add_internal_request_property(PayiPropertyNames.aws_bedrock_guardrail_id, guardrail_id)
@@ -524,7 +578,7 @@ class _BedrockConverseProviderRequest(_BedrockProviderRequest):
524
578
  @override
525
579
  def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool:
526
580
  super().process_request(instance, extra_headers, args, kwargs)
527
-
581
+
528
582
  guardrail_config = kwargs.get("guardrailConfig", {})
529
583
  if guardrail_config:
530
584
  guardrailIdentifier = guardrail_config.get("guardrailIdentifier", "")
@@ -1,15 +1,23 @@
1
1
  import json
2
- from typing import Any, Union, Optional, Sequence
2
+ from typing import Any, Dict, Union, Optional, Sequence
3
3
  from typing_extensions import override
4
4
  from importlib.metadata import version
5
5
 
6
6
  import tiktoken # type: ignore
7
7
  from wrapt import wrap_function_wrapper # type: ignore
8
8
 
9
- from payi.lib.helpers import PayiCategories, PayiHeaderNames
9
+ from payi.lib.helpers import PayiCategories
10
10
  from payi.types.ingest_units_params import Units
11
11
 
12
- from .instrument import _ChunkResult, _IsStreaming, _StreamingType, _ProviderRequest, _PayiInstrumentor
12
+ from .instrument import (
13
+ PayiInstrumentAzureOpenAiConfig,
14
+ _Context,
15
+ _ChunkResult,
16
+ _IsStreaming,
17
+ _StreamingType,
18
+ _ProviderRequest,
19
+ _PayiInstrumentor,
20
+ )
13
21
  from .version_helper import get_version_helper
14
22
 
15
23
 
@@ -17,12 +25,20 @@ class OpenAiInstrumentor:
17
25
  _module_name: str = "openai"
18
26
  _module_version: str = ""
19
27
 
28
+ _azure_openai_deployments: Dict[str, _Context] = {}
29
+
20
30
  @staticmethod
21
31
  def is_azure(instance: Any) -> bool:
22
32
  from openai import AzureOpenAI, AsyncAzureOpenAI # type: ignore # noqa: I001
23
33
 
24
34
  return isinstance(instance._client, (AsyncAzureOpenAI, AzureOpenAI))
25
35
 
36
+ @staticmethod
37
+ def configure(azure_openai_config: Optional[PayiInstrumentAzureOpenAiConfig]) -> None:
38
+ if azure_openai_config:
39
+ model_mappings = azure_openai_config.get("model_mappings", [])
40
+ OpenAiInstrumentor._azure_openai_deployments = _PayiInstrumentor._model_mapping_to_context_dict(model_mappings)
41
+
26
42
  @staticmethod
27
43
  def instrument(instrumentor: _PayiInstrumentor) -> None:
28
44
  try:
@@ -52,7 +68,7 @@ class OpenAiInstrumentor:
52
68
  aembeddings_wrapper(instrumentor),
53
69
  )
54
70
  except Exception as e:
55
- instrumentor._logger.debug(f"Error instrumenting openai: {e}")
71
+ instrumentor._logger.debug(f"Error instrumenting openai completions: {e}")
56
72
 
57
73
  # responses separately as they are relatively new and the client may not be using the latest openai module
58
74
  try:
@@ -69,7 +85,7 @@ class OpenAiInstrumentor:
69
85
  )
70
86
 
71
87
  except Exception as e:
72
- instrumentor._logger.debug(f"Error instrumenting openai: {e}")
88
+ instrumentor._logger.debug(f"Error instrumenting openai responses: {e}")
73
89
 
74
90
  @_PayiInstrumentor.payi_wrapper
75
91
  def embeddings_wrapper(
@@ -201,44 +217,39 @@ class _OpenAiProviderRequest(_ProviderRequest):
201
217
  self._input_tokens_details_key = input_tokens_details_key
202
218
 
203
219
  @override
204
- def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool: # type: ignore
205
- self._ingest["resource"] = kwargs.get("model", "")
220
+ def process_request(self, instance: Any, extra_headers: 'dict[str, str]', args: Sequence[Any], kwargs: Any) -> bool: # type: ignore
221
+ model = kwargs.get("model", "")
206
222
 
207
223
  if not (instance and hasattr(instance, "_client")) or OpenAiInstrumentor.is_azure(instance) is False:
224
+ self._ingest["resource"] = model
208
225
  return True
209
226
 
210
- context = self._instrumentor.get_context_safe()
211
- price_as_category = extra_headers.get(PayiHeaderNames.price_as_category) or context.get("price_as_category")
212
- price_as_resource = extra_headers.get(PayiHeaderNames.price_as_resource) or context.get("price_as_resource")
213
- resource_scope = extra_headers.get(PayiHeaderNames.resource_scope) or context.get("resource_scope")
214
-
215
- if PayiHeaderNames.price_as_category in extra_headers:
216
- del extra_headers[PayiHeaderNames.price_as_category]
217
- if PayiHeaderNames.price_as_resource in extra_headers:
218
- del extra_headers[PayiHeaderNames.price_as_resource]
219
- if PayiHeaderNames.resource_scope in extra_headers:
220
- del extra_headers[PayiHeaderNames.resource_scope]
221
-
222
- if not price_as_resource and not price_as_category:
227
+ if not self._price_as.resource and not self._price_as.category and OpenAiInstrumentor._azure_openai_deployments:
228
+ deployment = OpenAiInstrumentor._azure_openai_deployments.get(model, {})
229
+ self._price_as.category = deployment.get("price_as_category", None)
230
+ self._price_as.resource = deployment.get("price_as_resource", None)
231
+ self._price_as.resource_scope = deployment.get("resource_scope", None)
232
+
233
+ if not self._price_as.resource and not self._price_as.category:
223
234
  self._instrumentor._logger.error("Azure OpenAI requires price as resource and/or category to be specified, not ingesting")
224
235
  return False
225
236
 
226
- if resource_scope:
227
- if not(resource_scope in ["global", "datazone"] or resource_scope.startswith("region")):
237
+ if self._price_as.resource_scope:
238
+ if not (self._price_as.resource_scope in ["global", "datazone"] or self._price_as.resource_scope.startswith("region")):
228
239
  self._instrumentor._logger.error("Azure OpenAI invalid resource scope, not ingesting")
229
240
  return False
230
241
 
231
- self._ingest["resource_scope"] = resource_scope
242
+ self._ingest["resource_scope"] = self._price_as.resource_scope
232
243
 
233
244
  self._category = PayiCategories.azure_openai
234
245
 
235
246
  self._ingest["category"] = self._category
236
247
 
237
- if price_as_category:
248
+ if self._price_as.category:
238
249
  # price as category overrides default
239
- self._ingest["category"] = price_as_category
240
- if price_as_resource:
241
- self._ingest["resource"] = price_as_resource
250
+ self._ingest["category"] = self._price_as.category
251
+ if self._price_as.resource:
252
+ self._ingest["resource"] = self._price_as.resource
242
253
 
243
254
  return True
244
255
 
payi/lib/helpers.py CHANGED
@@ -1,15 +1,18 @@
1
1
  import os
2
- from typing import Dict, List, Union
2
+ import json
3
+ from typing import Any, Dict, List, Union
3
4
 
4
5
  PAYI_BASE_URL = "https://api.pay-i.com"
5
6
 
6
7
  class PayiHeaderNames:
7
8
  limit_ids:str = "xProxy-Limit-IDs"
8
9
  request_tags:str = "xProxy-Request-Tags"
10
+ request_properties:str = "xProxy-Request-Properties"
9
11
  use_case_id:str = "xProxy-UseCase-ID"
10
12
  use_case_name:str = "xProxy-UseCase-Name"
11
13
  use_case_version:str = "xProxy-UseCase-Version"
12
14
  use_case_step:str = "xProxy-UseCase-Step"
15
+ use_case_properties:str = "xProxy-UseCase-Properties"
13
16
  user_id:str = "xProxy-User-ID"
14
17
  account_name:str = "xProxy-Account-Name"
15
18
  price_as_category:str = "xProxy-PriceAs-Category"
@@ -37,6 +40,11 @@ class PayiPropertyNames:
37
40
  aws_bedrock_guardrail_version:str = "system.aws.bedrock.guardrail.version"
38
41
  aws_bedrock_guardrail_action:str = "system.aws.bedrock.guardrail.action"
39
42
 
43
+ class PayiResourceScopes:
44
+ global_scope: str = "global"
45
+ datazone_scope: str = "datazone"
46
+ region_scope: str = "region"
47
+
40
48
  def create_limit_header_from_ids(*, limit_ids: List[str]) -> Dict[str, str]:
41
49
  if not isinstance(limit_ids, list): # type: ignore
42
50
  raise TypeError("limit_ids must be a list")
@@ -53,6 +61,9 @@ def create_request_header_from_tags(*, request_tags: List[str]) -> Dict[str, str
53
61
 
54
62
  return { PayiHeaderNames.request_tags: ",".join(valid_tags) } if valid_tags else {}
55
63
 
64
+ def _compact_json(data: Any) -> str:
65
+ return json.dumps(data, separators=(',', ':'))
66
+
56
67
  def create_headers(
57
68
  *,
58
69
  limit_ids: Union[List[str], None] = None,
@@ -63,6 +74,8 @@ def create_headers(
63
74
  use_case_name: Union[str, None] = None,
64
75
  use_case_version: Union[int, None] = None,
65
76
  use_case_step: Union[str, None] = None,
77
+ use_case_properties: Union[Dict[str, str], None] = None,
78
+ request_properties: Union[Dict[str, str], None] = None,
66
79
  price_as_category: Union[str, None] = None,
67
80
  price_as_resource: Union[str, None] = None,
68
81
  resource_scope: Union[str, None] = None,
@@ -83,6 +96,10 @@ def create_headers(
83
96
  headers.update({ PayiHeaderNames.use_case_name: use_case_name})
84
97
  if use_case_version:
85
98
  headers.update({ PayiHeaderNames.use_case_version: str(use_case_version)})
99
+ if use_case_properties:
100
+ headers.update({ PayiHeaderNames.use_case_properties: _compact_json(use_case_properties) })
101
+ if request_properties:
102
+ headers.update({ PayiHeaderNames.request_properties: _compact_json(request_properties) })
86
103
  if use_case_step:
87
104
  headers.update({ PayiHeaderNames.use_case_step: use_case_step})
88
105
  if price_as_category:
payi/lib/instrument.py CHANGED
@@ -19,7 +19,7 @@ from wrapt import ObjectProxy # type: ignore
19
19
 
20
20
  from payi import Payi, AsyncPayi, APIStatusError, APIConnectionError, __version__ as _payi_version
21
21
  from payi.types import IngestUnitsParams
22
- from payi.lib.helpers import PayiHeaderNames, PayiPropertyNames
22
+ from payi.lib.helpers import PayiHeaderNames, PayiPropertyNames, _compact_json
23
23
  from payi.types.shared import XproxyResult
24
24
  from payi.types.ingest_response import IngestResponse
25
25
  from payi.types.ingest_units_params import Units, ProviderResponseFunctionCall
@@ -36,7 +36,13 @@ _g_logger: logging.Logger = logging.getLogger("payi.instrument")
36
36
  class _ChunkResult:
37
37
  send_chunk_to_caller: bool
38
38
  ingest: bool = False
39
-
39
+
40
+ @dataclass
41
+ class PriceAs:
42
+ category: Optional[str]
43
+ resource: Optional[str]
44
+ resource_scope: Optional[str]
45
+
40
46
  class _ProviderRequest:
41
47
  def __init__(
42
48
  self,
@@ -62,6 +68,7 @@ class _ProviderRequest:
62
68
  self._function_calls: Optional[list[ProviderResponseFunctionCall]] = None
63
69
  self._is_large_context: bool = False
64
70
  self._internal_request_properties: dict[str, Optional[str]] = {}
71
+ self._price_as: PriceAs = PriceAs(category=None, resource=None, resource_scope=None)
65
72
 
66
73
  def process_chunk(self, _chunk: Any) -> _ChunkResult:
67
74
  return _ChunkResult(send_chunk_to_caller=True)
@@ -147,8 +154,20 @@ class _ProviderRequest:
147
154
  self._ingest["provider_response_function_calls"] = self._function_calls
148
155
  self._function_calls.append(ProviderResponseFunctionCall(name=name, arguments=arguments))
149
156
 
157
+ class PayiInstrumentModelMapping(TypedDict, total=False):
158
+ model: str
159
+ price_as_category: Optional[str]
160
+ price_as_resource: Optional[str]
161
+ # "global", "datazone", "region", "region.<region_name>"
162
+ resource_scope: Optional[str]
163
+
150
164
  class PayiInstrumentAwsBedrockConfig(TypedDict, total=False):
151
- guardrail_trace: bool
165
+ guardrail_trace: Optional[bool]
166
+ model_mappings: Optional[Sequence[PayiInstrumentModelMapping]]
167
+
168
+ class PayiInstrumentAzureOpenAiConfig(TypedDict, total=False):
169
+ # map deployment name known model
170
+ model_mappings: Sequence[PayiInstrumentModelMapping]
152
171
 
153
172
  class PayiInstrumentOfflineInstrumentationConfig(TypedDict, total=False):
154
173
  file_name: str
@@ -168,6 +187,7 @@ class PayiInstrumentConfig(TypedDict, total=False):
168
187
  request_tags: Optional["list[str]"]
169
188
  request_properties: Optional["dict[str, Optional[str]]"]
170
189
  aws_config: Optional[PayiInstrumentAwsBedrockConfig]
190
+ azure_openai_config: Optional[PayiInstrumentAzureOpenAiConfig]
171
191
  offline_instrumentation: Optional[PayiInstrumentOfflineInstrumentationConfig]
172
192
 
173
193
  class PayiContext(TypedDict, total=False):
@@ -186,6 +206,19 @@ class PayiContext(TypedDict, total=False):
186
206
  resource_scope: Optional[str]
187
207
  last_result: Optional[Union[XproxyResult, XproxyError]]
188
208
 
209
+ class PayiInstanceDefaultContext(TypedDict, total=False):
210
+ use_case_name: Optional[str]
211
+ use_case_id: Optional[str]
212
+ use_case_version: Optional[int]
213
+ use_case_properties: Optional["dict[str, str]"]
214
+ limit_ids: Optional['list[str]']
215
+ user_id: Optional[str]
216
+ account_name: Optional[str]
217
+ request_properties: Optional["dict[str, str]"]
218
+ price_as_category: Optional[str]
219
+ price_as_resource: Optional[str]
220
+ resource_scope: Optional[str]
221
+
189
222
  class _Context(TypedDict, total=False):
190
223
  proxy: Optional[bool]
191
224
  use_case_name: Optional[str]
@@ -277,7 +310,8 @@ class _PayiInstrumentor:
277
310
  self._blocked_limits: set[str] = set()
278
311
  self._exceeded_limits: set[str] = set()
279
312
 
280
- self._api_connection_error_last_log_time: float = time.time()
313
+ # by not setting to time.time() the first connection error is always logged
314
+ self._api_connection_error_last_log_time: float = 0
281
315
  self._api_connection_error_count: int = 0
282
316
  self._api_connection_error_window: int = global_config.get("connection_error_logging_window", 60)
283
317
  if self._api_connection_error_window < 0:
@@ -303,10 +337,21 @@ class _PayiInstrumentor:
303
337
 
304
338
  global_instrumentation = global_config.pop("global_instrumentation", True)
305
339
 
340
+ # configure first, then instrument
341
+ aws_config = global_config.get("aws_config", None)
342
+ if aws_config:
343
+ from .BedrockInstrumentor import BedrockInstrumentor
344
+ BedrockInstrumentor.configure(aws_config=aws_config)
345
+
346
+ azure_openai_config = global_config.get("azure_openai_config", None)
347
+ if azure_openai_config:
348
+ from .OpenAIInstrumentor import OpenAiInstrumentor
349
+ OpenAiInstrumentor.configure(azure_openai_config=azure_openai_config)
350
+
306
351
  if instruments is None or "*" in instruments:
307
- self._instrument_all(global_config=global_config)
352
+ self._instrument_all()
308
353
  else:
309
- self._instrument_specific(instruments=instruments, global_config=global_config)
354
+ self._instrument_specific(instruments=instruments)
310
355
 
311
356
  if global_instrumentation:
312
357
  if "proxy" not in global_config:
@@ -338,13 +383,13 @@ class _PayiInstrumentor:
338
383
  context_keys = list(_Context.__annotations__.keys()) if hasattr(_Context, '__annotations__') else []
339
384
  for key in context_keys:
340
385
  if key in global_config:
341
- context[key] = global_config[key] # type: ignore
386
+ context[key] = global_config[key] # type: ignore[literal-required]
342
387
 
343
388
  self._init_current_context(**context)
344
389
 
345
390
  # Store the initialized context as the global initial context (immutable after this point)
346
391
  # All threads will inherit a copy of this context on their first access
347
- current_context = self.get_context()
392
+ current_context = self._context
348
393
  self._global_initial_context = current_context.copy() if current_context else None
349
394
 
350
395
  def _ensure_payi_clients(self) -> None:
@@ -355,20 +400,20 @@ class _PayiInstrumentor:
355
400
  self._payi = Payi()
356
401
  self._apayi = AsyncPayi()
357
402
 
358
- def _instrument_all(self, global_config: PayiInstrumentConfig) -> None:
403
+ def _instrument_all(self) -> None:
359
404
  self._instrument_openai()
360
405
  self._instrument_anthropic()
361
- self._instrument_aws_bedrock(global_config.get("aws_config", None))
406
+ self._instrument_aws_bedrock()
362
407
  self._instrument_google_vertex()
363
408
  self._instrument_google_genai()
364
409
 
365
- def _instrument_specific(self, instruments: Set[str], global_config: PayiInstrumentConfig) -> None:
410
+ def _instrument_specific(self, instruments: Set[str]) -> None:
366
411
  if PayiCategories.openai in instruments or PayiCategories.azure_openai in instruments:
367
412
  self._instrument_openai()
368
413
  if PayiCategories.anthropic in instruments:
369
414
  self._instrument_anthropic()
370
415
  if PayiCategories.aws_bedrock in instruments:
371
- self._instrument_aws_bedrock(global_config.get("aws_config", None))
416
+ self._instrument_aws_bedrock()
372
417
  if PayiCategories.google_vertex in instruments:
373
418
  self._instrument_google_vertex()
374
419
  self._instrument_google_genai()
@@ -391,11 +436,11 @@ class _PayiInstrumentor:
391
436
  except Exception as e:
392
437
  self._logger.error(f"Error instrumenting Anthropic: {e}")
393
438
 
394
- def _instrument_aws_bedrock(self, aws_config: Optional[PayiInstrumentAwsBedrockConfig]) -> None:
439
+ def _instrument_aws_bedrock(self) -> None:
395
440
  from .BedrockInstrumentor import BedrockInstrumentor
396
441
 
397
442
  try:
398
- BedrockInstrumentor.instrument(self, aws_config=aws_config)
443
+ BedrockInstrumentor.instrument(self)
399
444
 
400
445
  except Exception as e:
401
446
  self._logger.error(f"Error instrumenting AWS bedrock: {e}")
@@ -418,6 +463,28 @@ class _PayiInstrumentor:
418
463
  except Exception as e:
419
464
  self._logger.error(f"Error instrumenting Google GenAi: {e}")
420
465
 
466
+ @staticmethod
467
+ def _model_mapping_to_context_dict(model_mappings: Sequence[PayiInstrumentModelMapping]) -> 'dict[str, _Context]':
468
+ context: dict[str, _Context] = {}
469
+ for mapping in model_mappings:
470
+ model = mapping.get("model", "")
471
+ if not model:
472
+ continue
473
+
474
+ price_as_category = mapping.get("price_as_category", None)
475
+ price_as_resource = mapping.get("price_as_resource", None)
476
+ resource_scope = mapping.get("resource_scope", None)
477
+
478
+ if not price_as_category and not price_as_resource:
479
+ continue
480
+
481
+ context[model] = _Context(
482
+ price_as_category=price_as_category,
483
+ price_as_resource=price_as_resource,
484
+ resource_scope=resource_scope,
485
+ )
486
+ return context
487
+
421
488
  def _write_offline_ingest_packets(self) -> None:
422
489
  if not self._offline_instrumentation_file_name or not self._offline_ingest_packets:
423
490
  return
@@ -458,7 +525,17 @@ class _PayiInstrumentor:
458
525
 
459
526
  return log_ingest_units
460
527
 
461
- def _process_ingest_units(
528
+ def _merge_internal_request_properties(self, request: _ProviderRequest) -> None:
529
+ if not request._internal_request_properties:
530
+ return
531
+
532
+ properties = request._ingest.get("properties") or {}
533
+ request._ingest["properties"] = properties
534
+ for key, value in request._internal_request_properties.items():
535
+ if key not in properties:
536
+ properties[key] = value
537
+
538
+ def _after_invoke_update_request(
462
539
  self,
463
540
  request: _ProviderRequest,
464
541
  extra_headers: 'dict[str, str]') -> None:
@@ -477,12 +554,7 @@ class _PayiInstrumentor:
477
554
  if 'resource' not in ingest_units or ingest_units['resource'] == '':
478
555
  ingest_units['resource'] = "system.unknown_model"
479
556
 
480
- if request._internal_request_properties:
481
- properties = ingest_units.get("properties") or {}
482
- ingest_units["properties"] = properties
483
- for key, value in request._internal_request_properties.items():
484
- if key not in properties:
485
- properties[key] = value
557
+ self._merge_internal_request_properties(request)
486
558
 
487
559
  request_json = ingest_units.get('provider_request_json', "")
488
560
  if request_json and self._instrument_inline_data is False:
@@ -491,7 +563,7 @@ class _PayiInstrumentor:
491
563
  if request.remove_inline_data(prompt_dict):
492
564
  self._logger.debug(f"Removed inline data from provider_request_json")
493
565
  # store the modified dict back as JSON string
494
- ingest_units['provider_request_json'] = json.dumps(prompt_dict)
566
+ ingest_units['provider_request_json'] = _compact_json(prompt_dict)
495
567
 
496
568
  except Exception as e:
497
569
  self._logger.error(f"Error serializing provider_request_json: {e}")
@@ -544,7 +616,7 @@ class _PayiInstrumentor:
544
616
  self._logger.debug(f"_aingest_units")
545
617
 
546
618
  extra_headers: 'dict[str, str]' = {}
547
- self._process_ingest_units(request, extra_headers=extra_headers)
619
+ self._after_invoke_update_request(request, extra_headers=extra_headers)
548
620
 
549
621
  try:
550
622
  if self._logger.isEnabledFor(logging.DEBUG):
@@ -668,7 +740,7 @@ class _PayiInstrumentor:
668
740
  self._logger.debug(f"_ingest_units")
669
741
 
670
742
  extra_headers: 'dict[str, str]' = {}
671
- self._process_ingest_units(request, extra_headers=extra_headers)
743
+ self._after_invoke_update_request(request, extra_headers=extra_headers)
672
744
 
673
745
  try:
674
746
  if self._payi:
@@ -763,6 +835,56 @@ class _PayiInstrumentor:
763
835
  else:
764
836
  return value.copy()
765
837
 
838
+ def _set_instance_default_context(
839
+ self,
840
+ instance: Any,
841
+ context: PayiInstanceDefaultContext
842
+ ) -> None:
843
+ if instance is None:
844
+ raise ValueError("instance cannot be None")
845
+ if not context:
846
+ raise ValueError("context_dict cannot be None or empty")
847
+
848
+ context = context.copy()
849
+ if "use_case_properties" in context and context["use_case_properties"] is not None:
850
+ context["use_case_properties"] = context["use_case_properties"].copy()
851
+ if "request_properties" in context and context["request_properties"] is not None:
852
+ context["request_properties"] = context["request_properties"].copy()
853
+ if "limit_ids" in context and context["limit_ids"] is not None:
854
+ context["limit_ids"] = context["limit_ids"].copy()
855
+
856
+ instance.__payi_default_context__ = context
857
+ self._logger.debug(f"payi_set_default_context: attached context to instance {type(instance).__name__}")
858
+
859
+ @staticmethod
860
+ def _get_instance_default_context(
861
+ instance: Any
862
+ ) -> "Optional[PayiInstanceDefaultContext]":
863
+ if instance is None:
864
+ return None
865
+
866
+ context = getattr(instance, "__payi_default_context__", None)
867
+ if not context:
868
+ inner_instance = getattr(instance, "_client", None)
869
+ if inner_instance:
870
+ context = getattr(inner_instance, "__payi_default_context__", None)
871
+
872
+ # Return a copy to prevent external modifications
873
+ return context if context else None
874
+
875
+ @staticmethod
876
+ def _merge_context_instance_defaults(
877
+ context: _Context,
878
+ instance_defaults: Optional[PayiInstanceDefaultContext]
879
+ ) -> _Context:
880
+ if instance_defaults:
881
+ context = context.copy()
882
+ for key, value in instance_defaults.items():
883
+ if value is not None and context.get(key, None) is None:
884
+ context[key] = value # type: ignore[literal-required]
885
+
886
+ return context
887
+
766
888
  def _init_current_context(
767
889
  self,
768
890
  proxy: Optional[bool] = None,
@@ -781,7 +903,7 @@ class _PayiInstrumentor:
781
903
  ) -> None:
782
904
 
783
905
  # there will always be a current context
784
- context: _Context = self.get_context() # type: ignore
906
+ context: _Context = self._context # type: ignore
785
907
  parent_context: _Context = self._context_stack[-2] if len(self._context_stack) > 1 else {}
786
908
 
787
909
  parent_proxy = parent_context.get("proxy", self._proxy_default)
@@ -923,27 +1045,38 @@ class _PayiInstrumentor:
923
1045
  if self._context_stack:
924
1046
  self._context_stack.pop()
925
1047
 
926
- def get_context(self) -> Optional[_Context]:
1048
+ @property
1049
+ def _context(self) -> Optional[_Context]:
927
1050
  # Return the current top of the stack
928
1051
  return self._context_stack[-1] if self._context_stack else None
929
1052
 
930
- def get_context_safe(self) -> _Context:
1053
+ @property
1054
+ def _context_safe(self) -> _Context:
931
1055
  # Return the current top of the stack
932
- return self.get_context() or {}
1056
+ return self._context or {}
1057
+
1058
+ def _extract_price_as(self, extra_headers: "dict[str, str]") -> PriceAs:
1059
+ context = self._context_safe
933
1060
 
934
- def _prepare_ingest(
1061
+ return PriceAs(
1062
+ category=extra_headers.pop(PayiHeaderNames.price_as_category, None) or context.get("price_as_category", None),
1063
+ resource=extra_headers.pop(PayiHeaderNames.price_as_resource, None) or context.get("price_as_resource", None),
1064
+ resource_scope=extra_headers.pop(PayiHeaderNames.resource_scope, None) or context.get("resource_scope", None),
1065
+ )
1066
+
1067
+ def _before_invoke_update_request(
935
1068
  self,
936
1069
  request: _ProviderRequest,
937
- context: _Context,
938
1070
  ingest_extra_headers: "dict[str, str]", # do not conflict with potential kwargs["extra_headers"]
939
1071
  args: Sequence[Any],
940
1072
  kwargs: 'dict[str, Any]',
941
1073
  ) -> None:
942
1074
 
943
- limit_ids = ingest_extra_headers.pop(PayiHeaderNames.limit_ids, None)
944
1075
  # pop and ignore the request tags header since it is no longer processed
945
1076
  ingest_extra_headers.pop(PayiHeaderNames.request_tags, None)
946
1077
 
1078
+ limit_ids = ingest_extra_headers.pop(PayiHeaderNames.limit_ids, None)
1079
+
947
1080
  use_case_name = ingest_extra_headers.pop(PayiHeaderNames.use_case_name, None)
948
1081
  use_case_id = ingest_extra_headers.pop(PayiHeaderNames.use_case_id, None)
949
1082
  use_case_version = ingest_extra_headers.pop(PayiHeaderNames.use_case_version, None)
@@ -952,6 +1085,9 @@ class _PayiInstrumentor:
952
1085
  user_id = ingest_extra_headers.pop(PayiHeaderNames.user_id, None)
953
1086
  account_name = ingest_extra_headers.pop(PayiHeaderNames.account_name, None)
954
1087
 
1088
+ request_properties = ingest_extra_headers.pop(PayiHeaderNames.request_properties, "")
1089
+ use_case_properties = ingest_extra_headers.pop(PayiHeaderNames.use_case_properties, "")
1090
+
955
1091
  if limit_ids:
956
1092
  request._ingest["limit_ids"] = limit_ids.split(",")
957
1093
  if use_case_name:
@@ -966,23 +1102,10 @@ class _PayiInstrumentor:
966
1102
  request._ingest["user_id"] = user_id
967
1103
  if account_name:
968
1104
  request._ingest["account_name"] = account_name
969
-
970
- request_properties = context.get("request_properties", None)
971
1105
  if request_properties:
972
- request._ingest["properties"] = request_properties
973
-
974
- use_case_properties = context.get("use_case_properties", None)
1106
+ request._ingest["properties"] = json.loads(request_properties)
975
1107
  if use_case_properties:
976
- request._ingest["use_case_properties"] = use_case_properties
977
-
978
- if request._internal_request_properties:
979
- if "properties" in request._ingest and request._ingest["properties"] is not None:
980
- # Merge internal request properties, but don't override existing keys
981
- for key, value in request._internal_request_properties.items():
982
- if key not in request._ingest["properties"]:
983
- request._ingest["properties"][key] = value
984
- else:
985
- request._ingest["properties"] = request._internal_request_properties # Assign
1108
+ request._ingest["use_case_properties"] = json.loads(use_case_properties)
986
1109
 
987
1110
  if len(ingest_extra_headers) > 0:
988
1111
  request._ingest["provider_request_headers"] = [PayICommonModelsAPIRouterHeaderInfoParam(name=k, value=v) for k, v in ingest_extra_headers.items()]
@@ -1006,7 +1129,7 @@ class _PayiInstrumentor:
1006
1129
  request.process_request_prompt(provider_prompt, args, kwargs)
1007
1130
 
1008
1131
  if self._log_prompt_and_response:
1009
- request._ingest["provider_request_json"] = json.dumps(provider_prompt)
1132
+ request._ingest["provider_request_json"] = _compact_json(provider_prompt)
1010
1133
 
1011
1134
  request._ingest["event_timestamp"] = datetime.now(timezone.utc)
1012
1135
 
@@ -1021,7 +1144,7 @@ class _PayiInstrumentor:
1021
1144
  ) -> Any:
1022
1145
  self._logger.debug(f"async_invoke_wrapper: instance {instance}, category {request._category}")
1023
1146
 
1024
- context = self.get_context()
1147
+ context = self._context
1025
1148
 
1026
1149
  # Bedrock client does not have an async method
1027
1150
 
@@ -1031,6 +1154,8 @@ class _PayiInstrumentor:
1031
1154
  # wrapped function invoked outside of decorator scope
1032
1155
  return await wrapped(*args, **kwargs)
1033
1156
 
1157
+ # context = self._merge_context_instance_defaults(context, self._get_instance_default_context(instance))
1158
+
1034
1159
  # after _udpate_headers, all metadata to add to ingest is in extra_headers, keyed by the xproxy-xxx header name
1035
1160
  extra_headers: Optional[dict[str, str]] = kwargs.get("extra_headers")
1036
1161
  extra_headers = (extra_headers or {}).copy()
@@ -1046,12 +1171,16 @@ class _PayiInstrumentor:
1046
1171
  self._logger.debug(f"async_invoke_wrapper: sending proxy request")
1047
1172
 
1048
1173
  return await wrapped(*args, **kwargs)
1174
+
1175
+ request._price_as = self._extract_price_as(extra_headers)
1176
+ if not request.supports_extra_headers and "extra_headers" in kwargs:
1177
+ kwargs.pop("extra_headers", None)
1049
1178
 
1050
1179
  current_frame = inspect.currentframe()
1051
1180
  # f_back excludes the current frame, strip() cleans up whitespace and newlines
1052
1181
  stack = [frame.strip() for frame in traceback.format_stack(current_frame.f_back)] # type: ignore
1053
1182
 
1054
- request._ingest['properties'] = { 'system.stack_trace': json.dumps(stack) }
1183
+ request._ingest['properties'] = { 'system.stack_trace': _compact_json(stack) }
1055
1184
 
1056
1185
  if request.process_request(instance, extra_headers, args, kwargs) is False:
1057
1186
  self._logger.debug(f"async_invoke_wrapper: calling wrapped instance")
@@ -1068,7 +1197,7 @@ class _PayiInstrumentor:
1068
1197
  stream = False
1069
1198
 
1070
1199
  try:
1071
- self._prepare_ingest(request, context, extra_headers, args, kwargs)
1200
+ self._before_invoke_update_request(request, extra_headers, args, kwargs)
1072
1201
  self._logger.debug(f"async_invoke_wrapper: calling wrapped instance (stream={stream})")
1073
1202
 
1074
1203
  if "extra_headers" in kwargs:
@@ -1146,7 +1275,7 @@ class _PayiInstrumentor:
1146
1275
  ) -> Any:
1147
1276
  self._logger.debug(f"invoke_wrapper: instance {instance}, category {request._category}")
1148
1277
 
1149
- context = self.get_context()
1278
+ context = self._context
1150
1279
 
1151
1280
  if not context:
1152
1281
  if not request.supports_extra_headers:
@@ -1157,6 +1286,8 @@ class _PayiInstrumentor:
1157
1286
  # wrapped function invoked outside of decorator scope
1158
1287
  return wrapped(*args, **kwargs)
1159
1288
 
1289
+ # context = self._merge_context_instance_defaults(context, self._get_instance_default_context(instance))
1290
+
1160
1291
  # after _udpate_headers, all metadata to add to ingest is in extra_headers, keyed by the xproxy-xxx header name
1161
1292
  extra_headers: Optional[dict[str, str]] = kwargs.get("extra_headers")
1162
1293
  extra_headers = (extra_headers or {}).copy()
@@ -1172,12 +1303,16 @@ class _PayiInstrumentor:
1172
1303
  self._logger.debug(f"invoke_wrapper: sending proxy request")
1173
1304
 
1174
1305
  return wrapped(*args, **kwargs)
1306
+
1307
+ request._price_as = self._extract_price_as(extra_headers)
1308
+ if not request.supports_extra_headers and "extra_headers" in kwargs:
1309
+ kwargs.pop("extra_headers", None)
1175
1310
 
1176
1311
  current_frame = inspect.currentframe()
1177
1312
  # f_back excludes the current frame, strip() cleans up whitespace and newlines
1178
1313
  stack = [frame.strip() for frame in traceback.format_stack(current_frame.f_back)] # type: ignore
1179
1314
 
1180
- request._ingest['properties'] = { 'system.stack_trace': json.dumps(stack) }
1315
+ request._ingest['properties'] = { 'system.stack_trace': _compact_json(stack) }
1181
1316
 
1182
1317
  if request.process_request(instance, extra_headers, args, kwargs) is False:
1183
1318
  self._logger.debug(f"invoke_wrapper: calling wrapped instance")
@@ -1194,7 +1329,7 @@ class _PayiInstrumentor:
1194
1329
  stream = False
1195
1330
 
1196
1331
  try:
1197
- self._prepare_ingest(request, context, extra_headers, args, kwargs)
1332
+ self._before_invoke_update_request(request, extra_headers, args, kwargs)
1198
1333
  self._logger.debug(f"invoke_wrapper: calling wrapped instance (stream={stream})")
1199
1334
 
1200
1335
  if "extra_headers" in kwargs:
@@ -1274,7 +1409,7 @@ class _PayiInstrumentor:
1274
1409
  self
1275
1410
  ) -> 'dict[str, str]':
1276
1411
  extra_headers: dict[str, str] = {}
1277
- context = self.get_context()
1412
+ context = self._context
1278
1413
  if context:
1279
1414
  self._update_extra_headers(context, extra_headers)
1280
1415
 
@@ -1303,13 +1438,38 @@ class _PayiInstrumentor:
1303
1438
  context_price_as_resource: Optional[str] = context.get("price_as_resource")
1304
1439
  context_resource_scope: Optional[str] = context.get("resource_scope")
1305
1440
 
1306
- # headers_limit_ids = extra_headers.get(PayiHeaderNames.limit_ids, None)
1307
-
1441
+ context_request_properties: Optional[dict[str, Optional[str]]] = context.get("request_properties")
1442
+ context_use_case_properties: Optional[dict[str, Optional[str]]] = context.get("use_case_properties")
1443
+
1444
+ if PayiHeaderNames.request_properties in extra_headers:
1445
+ headers_request_properties = extra_headers.get(PayiHeaderNames.request_properties, None)
1446
+
1447
+ if not headers_request_properties:
1448
+ # headers_request_properties is empty, remove it from extra_headers
1449
+ extra_headers.pop(PayiHeaderNames.request_properties, None)
1450
+ else:
1451
+ # leave the value in extra_headers
1452
+ ...
1453
+ elif context_request_properties:
1454
+ extra_headers[PayiHeaderNames.request_properties] = _compact_json(context_request_properties)
1455
+
1456
+ if PayiHeaderNames.use_case_properties in extra_headers:
1457
+ headers_use_case_properties = extra_headers.get(PayiHeaderNames.use_case_properties, None)
1458
+
1459
+ if not headers_use_case_properties:
1460
+ # headers_use_case_properties is empty, remove it from extra_headers
1461
+ extra_headers.pop(PayiHeaderNames.use_case_properties, None)
1462
+ else:
1463
+ # leave the value in extra_headers
1464
+ ...
1465
+ elif context_use_case_properties:
1466
+ extra_headers[PayiHeaderNames.use_case_properties] = _compact_json(context_use_case_properties)
1467
+
1308
1468
  # If the caller specifies limit_ids in extra_headers, it takes precedence over the decorator
1309
1469
  if PayiHeaderNames.limit_ids in extra_headers:
1310
1470
  headers_limit_ids = extra_headers.get(PayiHeaderNames.limit_ids)
1311
1471
 
1312
- if headers_limit_ids is None or len(headers_limit_ids) == 0:
1472
+ if not headers_limit_ids:
1313
1473
  # headers_limit_ids is empty, remove it from extra_headers
1314
1474
  extra_headers.pop(PayiHeaderNames.limit_ids, None)
1315
1475
  else:
@@ -1320,7 +1480,7 @@ class _PayiInstrumentor:
1320
1480
 
1321
1481
  if PayiHeaderNames.user_id in extra_headers:
1322
1482
  headers_user_id = extra_headers.get(PayiHeaderNames.user_id, None)
1323
- if headers_user_id is None or len(headers_user_id) == 0:
1483
+ if not headers_user_id:
1324
1484
  # headers_user_id is empty, remove it from extra_headers
1325
1485
  extra_headers.pop(PayiHeaderNames.user_id, None)
1326
1486
  else:
@@ -1331,7 +1491,7 @@ class _PayiInstrumentor:
1331
1491
 
1332
1492
  if PayiHeaderNames.account_name in extra_headers:
1333
1493
  headers_account_name = extra_headers.get(PayiHeaderNames.account_name, None)
1334
- if headers_account_name is None or len(headers_account_name) == 0:
1494
+ if not headers_account_name:
1335
1495
  # headers_account_name is empty, remove it from extra_headers
1336
1496
  extra_headers.pop(PayiHeaderNames.account_name, None)
1337
1497
  else:
@@ -1342,7 +1502,7 @@ class _PayiInstrumentor:
1342
1502
 
1343
1503
  if PayiHeaderNames.use_case_name in extra_headers:
1344
1504
  headers_use_case_name = extra_headers.get(PayiHeaderNames.use_case_name, None)
1345
- if headers_use_case_name is None or len(headers_use_case_name) == 0:
1505
+ if not headers_use_case_name:
1346
1506
  # headers_use_case_name is empty, remove all use case related headers
1347
1507
  extra_headers.pop(PayiHeaderNames.use_case_name, None)
1348
1508
  extra_headers.pop(PayiHeaderNames.use_case_id, None)
@@ -1604,7 +1764,7 @@ class _StreamIteratorWrapper(ObjectProxy): # type: ignore
1604
1764
  return chunk
1605
1765
  else:
1606
1766
  # assume dict
1607
- return json.dumps(chunk)
1767
+ return _compact_json(chunk)
1608
1768
 
1609
1769
  class _StreamManagerWrapper(ObjectProxy): # type: ignore
1610
1770
  def __init__(
@@ -1678,7 +1838,7 @@ class _GeneratorWrapper: # type: ignore
1678
1838
 
1679
1839
  if self._log_prompt_and_response:
1680
1840
  dict = self._chunk_to_dict(chunk)
1681
- self._responses.append(json.dumps(dict))
1841
+ self._responses.append(_compact_json(dict))
1682
1842
 
1683
1843
  return self._request.process_chunk(chunk)
1684
1844
 
@@ -1916,7 +2076,7 @@ def get_context() -> PayiContext:
1916
2076
  """
1917
2077
  if not _instrumentor:
1918
2078
  return PayiContext()
1919
- internal_context = _instrumentor.get_context() or {}
2079
+ internal_context = _instrumentor._context_safe
1920
2080
 
1921
2081
  context_dict = {
1922
2082
  key: value
@@ -1925,4 +2085,13 @@ def get_context() -> PayiContext:
1925
2085
  }
1926
2086
  if _instrumentor._last_result:
1927
2087
  context_dict["last_result"] = _instrumentor._last_result
1928
- return PayiContext(**dict(context_dict)) # type: ignore
2088
+ return PayiContext(**dict(context_dict)) # type: ignore
2089
+
2090
+ # def payi_set_default_context(
2091
+ # instance: Any,
2092
+ # context: PayiInstanceDefaultContext
2093
+ # ) -> None:
2094
+ # if not _instrumentor:
2095
+ # raise RuntimeError("payi_instrument() must be called before using payi_add_client_default_context()")
2096
+
2097
+ # _instrumentor._set_instance_default_context(instance, context)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: payi
3
- Version: 0.1.0a133
3
+ Version: 0.1.0a134
4
4
  Summary: The official Python library for the payi API
5
5
  Project-URL: Homepage, https://github.com/Pay-i/pay-i-python
6
6
  Project-URL: Repository, https://github.com/Pay-i/pay-i-python
@@ -11,7 +11,7 @@ payi/_resource.py,sha256=j2jIkTr8OIC8sU6-05nxSaCyj4MaFlbZrwlyg4_xJos,1088
11
11
  payi/_response.py,sha256=rh9oJAvCKcPwQFm4iqH_iVrmK8bNx--YP_A2a4kN1OU,28776
12
12
  payi/_streaming.py,sha256=r-qd1jFI1Df33CEelGocyNFdGg4C_NAK8TEX9GmxdOM,10141
13
13
  payi/_types.py,sha256=d6xrZDG6rG6opphTN7UVYdEOis3977LrQQgpNtklXZE,7234
14
- payi/_version.py,sha256=kw_Cm8vm327o_bU7L7yyNX5kwwFwX2nVPCsToTbFc6g,166
14
+ payi/_version.py,sha256=PfQ5Xib4CNlYWPqPk3ILAQTZxnM8F2pDXmLoDhpmUCQ,166
15
15
  payi/pagination.py,sha256=k2356QGPOUSjRF2vHpwLBdF6P-2vnQzFfRIJQAHGQ7A,1258
16
16
  payi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  payi/_utils/__init__.py,sha256=7fch0GT9zpNnErbciSpUNa-SjTxxjY6kxHxKMOM4AGs,2305
@@ -27,15 +27,15 @@ payi/_utils/_transform.py,sha256=NjCzmnfqYrsAikUHQig6N9QfuTVbKipuP3ur9mcNF-E,159
27
27
  payi/_utils/_typing.py,sha256=N_5PPuFNsaygbtA_npZd98SVN1LQQvFTKL6bkWPBZGU,4786
28
28
  payi/_utils/_utils.py,sha256=0dDqauUbVZEXV0NVl7Bwu904Wwo5eyFCZpQThhFNhyA,12253
29
29
  payi/lib/.keep,sha256=wuNrz-5SXo3jJaJOJgz4vFHM41YH_g20F5cRQo0vLes,224
30
- payi/lib/AnthropicInstrumentor.py,sha256=SI6nzU-eVufHbxV5gxgrZDhx3TewRVnsvtwDuwEG6WU,16484
31
- payi/lib/BedrockInstrumentor.py,sha256=-dF3mk2nsCOrYxeV75eBORxYEJ7Nd1nIJWsy4-2LEHk,27324
30
+ payi/lib/AnthropicInstrumentor.py,sha256=AyVwflcprDt7ah0fO7gtQjcwLiElxX_W-GnL6e86K7g,16971
31
+ payi/lib/BedrockInstrumentor.py,sha256=wc3R838Sp1Gr5GTe_S1egx8rG6kHllVZsnPZhX2sSYQ,29464
32
32
  payi/lib/GoogleGenAiInstrumentor.py,sha256=LHiEZ7G5IhCcDlpVzQlXW9Ok96MHLeq7emEhFzPBTm0,8836
33
- payi/lib/OpenAIInstrumentor.py,sha256=_ULwIli11XP1yZK_pMGXuaSmHZ5pozuEt_v5DfhNuGw,22914
33
+ payi/lib/OpenAIInstrumentor.py,sha256=3rR7EndaUXZrT1HSzfhAWIVjX2ha172un56HOqBsQf0,23183
34
34
  payi/lib/Stopwatch.py,sha256=7OJlxvr2Jyb6Zr1LYCYKczRB7rDVKkIR7gc4YoleNdE,764
35
35
  payi/lib/VertexInstrumentor.py,sha256=OWuMPiW4LdLhj6DSAAy5qZiosVo8DSAuFWGxYpEucoE,7431
36
36
  payi/lib/VertexRequest.py,sha256=42F7xCRYY6h3EMUZD1x4-_cwyAcVhnzT9M5zl4KwtE0,11801
37
- payi/lib/helpers.py,sha256=6c0RFMS0AYVIxU6q8ak1CDwMTwldIN7N2O2XkxTO7ag,4931
38
- payi/lib/instrument.py,sha256=Mc9ipigz21qvuM6tWC1GoYSr-zoJDBrGmxZm6DxqCbo,78765
37
+ payi/lib/helpers.py,sha256=kqKXVdHlTcK_OQgIwuDXKbtbmezDMxTv7Ax7O0oSTgk,5658
38
+ payi/lib/instrument.py,sha256=HhSQpAEwlEDwPhpZWZVTq2xHdZgy12W255DMlpkl24Y,85586
39
39
  payi/lib/version_helper.py,sha256=v0lC3kuaXn6PBDolE3mkmwJiA8Ot3z4RkVR7wlBuZCs,540
40
40
  payi/lib/data/cohere_embed_english_v3.json,sha256=YEWwjml3_i16cdsOx_7UKe6xpVFnxTEhP8T1n54R6gY,718306
41
41
  payi/resources/__init__.py,sha256=B2bn1ZfCf6TbHlzZvy5TpFPtALnFcBRPYVKQH3S5qfQ,2457
@@ -135,7 +135,7 @@ payi/types/use_cases/definitions/kpi_retrieve_response.py,sha256=uQXliSvS3k-yDYw
135
135
  payi/types/use_cases/definitions/kpi_update_params.py,sha256=jbawdWAdMnsTWVH0qfQGb8W7_TXe3lq4zjSRu44d8p8,373
136
136
  payi/types/use_cases/definitions/kpi_update_response.py,sha256=zLyEoT0S8d7XHsnXZYT8tM7yDw0Aze0Mk-_Z6QeMtc8,459
137
137
  payi/types/use_cases/definitions/limit_config_create_params.py,sha256=Y6RR7IYiTZHYJ4y6m2TmlAe4ArtbjEl7fIrAhnCvuPI,464
138
- payi-0.1.0a133.dist-info/METADATA,sha256=Y0bDz9gEnpdYXh7hMvgRGnnJ632FAYiKf4bnv1Fq6-c,16324
139
- payi-0.1.0a133.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
140
- payi-0.1.0a133.dist-info/licenses/LICENSE,sha256=CQt03aM-P4a3Yg5qBg3JSLVoQS3smMyvx7tYg_6V7Gk,11334
141
- payi-0.1.0a133.dist-info/RECORD,,
138
+ payi-0.1.0a134.dist-info/METADATA,sha256=Xgtk_iCsvrSL62RxLuRrqgOesuMmKsHpEgpBcWQWy6U,16324
139
+ payi-0.1.0a134.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
140
+ payi-0.1.0a134.dist-info/licenses/LICENSE,sha256=CQt03aM-P4a3Yg5qBg3JSLVoQS3smMyvx7tYg_6V7Gk,11334
141
+ payi-0.1.0a134.dist-info/RECORD,,