payi 0.1.0a125__py3-none-any.whl → 0.1.0a126__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.125" # x-release-please-version
4
+ __version__ = "0.1.0-alpha.126" # x-release-please-version
@@ -1,13 +1,12 @@
1
1
  import os
2
2
  import json
3
- from typing import Any, Optional, Sequence
3
+ from typing import TYPE_CHECKING, Any, Optional, Sequence
4
4
  from functools import wraps
5
5
  from typing_extensions import override
6
6
 
7
7
  from wrapt import ObjectProxy, wrap_function_wrapper # type: ignore
8
- from tokenizers import Tokenizer # type: ignore
9
8
 
10
- from payi.lib.helpers import PayiCategories, PayiHeaderNames, payi_aws_bedrock_url
9
+ from payi.lib.helpers import PayiCategories, PayiHeaderNames, PayiPropertyNames, payi_aws_bedrock_url
11
10
  from payi.types.ingest_units_params import Units
12
11
  from payi.types.pay_i_common_models_api_router_header_info_param import PayICommonModelsAPIRouterHeaderInfoParam
13
12
 
@@ -21,9 +20,10 @@ from .instrument import (
21
20
  )
22
21
  from .version_helper import get_version_helper
23
22
 
24
- GUARDRAIL_ID = "system.aws.bedrock.guardrail.id"
25
- GUARDRAIL_VERSION = "system.aws.bedrock.guardrail.version"
26
- GUARDRAIL_ACTION = "system.aws.bedrock.guardrail.action"
23
+ if TYPE_CHECKING:
24
+ from tokenizers import Tokenizer # type: ignore
25
+ else:
26
+ Tokenizer = None
27
27
 
28
28
  GUARDRAIL_SEMANTIC_FAILURE_DESCRIPTION = "Bedrock Guardrails intervened"
29
29
 
@@ -118,9 +118,8 @@ def _redirect_to_payi(request: Any, event_name: str, **_: 'dict[str, Any]') -> N
118
118
  for key, value in extra_headers.items():
119
119
  request.headers[key] = value
120
120
 
121
-
122
121
  class InvokeResponseWrapper(ObjectProxy): # type: ignore
123
- _cohere_embed_english_v3_tokenizer: Optional[Tokenizer] = None
122
+ _cohere_embed_english_v3_tokenizer: Optional['Tokenizer'] = None
124
123
 
125
124
  def __init__(
126
125
  self,
@@ -189,15 +188,26 @@ class InvokeResponseWrapper(ObjectProxy): # type: ignore
189
188
  if texts and len(texts) > 0:
190
189
  text = " ".join(texts)
191
190
 
192
- if self._cohere_embed_english_v3_tokenizer is None:
193
- current_dir = os.path.dirname(os.path.abspath(__file__))
194
- tokenizer_path = os.path.join(current_dir, "data", "cohere_embed_english_v3.json")
195
- self._cohere_embed_english_v3_tokenizer = Tokenizer.from_file(tokenizer_path) # type: ignore
191
+ try:
192
+ from tokenizers import Tokenizer # type: ignore
193
+
194
+ if self._cohere_embed_english_v3_tokenizer is None: # type: ignore
195
+ current_dir = os.path.dirname(os.path.abspath(__file__))
196
+ tokenizer_path = os.path.join(current_dir, "data", "cohere_embed_english_v3.json")
197
+ self._cohere_embed_english_v3_tokenizer = Tokenizer.from_file(tokenizer_path) # type: ignore
198
+
199
+ if self._cohere_embed_english_v3_tokenizer is not None and isinstance(self._cohere_embed_english_v3_tokenizer, Tokenizer): # type: ignore
200
+ tokens: list = self._cohere_embed_english_v3_tokenizer.encode(text, add_special_tokens=False).tokens # type: ignore
196
201
 
197
- tokens: list = self._cohere_embed_english_v3_tokenizer.encode(text, add_special_tokens=False).tokens # type: ignore
202
+ if tokens and isinstance(tokens, list):
203
+ units["text"] = Units(input=len(tokens), output=0) # type: ignore
198
204
 
199
- if tokens and isinstance(tokens, list):
200
- units["text"] = Units(input=len(tokens), output=0) # type: ignore
205
+ except ImportError:
206
+ self._request._instrumentor._logger.warning("tokenizers module not found, caller must install the tokenizers module. Cannot record text tokens for Cohere embed english v3")
207
+ pass
208
+ except Exception as e:
209
+ self._request._instrumentor._logger.warning(f"Error processing Cohere embed english v3 response: {e}")
210
+ pass
201
211
 
202
212
  if self._log_prompt_and_response:
203
213
  ingest["provider_response_json"] = data.decode('utf-8') # type: ignore
@@ -388,11 +398,11 @@ class _BedrockInvokeProviderRequest(_BedrockProviderRequest):
388
398
 
389
399
  guardrail_id = kwargs.get("guardrailIdentifier", "")
390
400
  if guardrail_id:
391
- self.add_internal_request_property(GUARDRAIL_ID, guardrail_id)
401
+ self.add_internal_request_property(PayiPropertyNames.aws_bedrock_guardrail_id, guardrail_id)
392
402
 
393
403
  guardrail_version = kwargs.get("guardrailVersion", "")
394
404
  if guardrail_version:
395
- self.add_internal_request_property(GUARDRAIL_VERSION, guardrail_version)
405
+ self.add_internal_request_property(PayiPropertyNames.aws_bedrock_guardrail_version, guardrail_version)
396
406
 
397
407
  if guardrail_id and guardrail_version and BedrockInstrumentor._guardrail_trace:
398
408
  trace = kwargs.get("trace", None)
@@ -488,9 +498,9 @@ class _BedrockInvokeProviderRequest(_BedrockProviderRequest):
488
498
  def process_stop_action(self, action: str) -> None:
489
499
  # record both as a semantic failure and guardrail action so it is discoverable through both properties
490
500
  if action == "INTERVENED":
491
- self.add_internal_request_property('system.failure', action)
492
- self.add_internal_request_property('system.failure.description', GUARDRAIL_SEMANTIC_FAILURE_DESCRIPTION)
493
- self.add_internal_request_property(GUARDRAIL_ACTION, action)
501
+ self.add_internal_request_property(PayiPropertyNames.failure, action)
502
+ self.add_internal_request_property(PayiPropertyNames.failure_description, GUARDRAIL_SEMANTIC_FAILURE_DESCRIPTION)
503
+ self.add_internal_request_property(PayiPropertyNames.aws_bedrock_guardrail_action, action)
494
504
 
495
505
  @override
496
506
  def remove_inline_data(self, prompt: 'dict[str, Any]') -> bool:# noqa: ARG002
@@ -519,11 +529,11 @@ class _BedrockConverseProviderRequest(_BedrockProviderRequest):
519
529
  if guardrail_config:
520
530
  guardrailIdentifier = guardrail_config.get("guardrailIdentifier", "")
521
531
  if guardrailIdentifier:
522
- self.add_internal_request_property(GUARDRAIL_ID, guardrailIdentifier)
532
+ self.add_internal_request_property(PayiPropertyNames.aws_bedrock_guardrail_id, guardrailIdentifier)
523
533
 
524
534
  guardrailVersion = guardrail_config.get("guardrailVersion", "")
525
535
  if guardrailVersion:
526
- self.add_internal_request_property(GUARDRAIL_VERSION, guardrailVersion)
536
+ self.add_internal_request_property(PayiPropertyNames.aws_bedrock_guardrail_version, guardrailVersion)
527
537
 
528
538
  if guardrailIdentifier and guardrailVersion and BedrockInstrumentor._guardrail_trace:
529
539
  trace = guardrail_config.get("trace", None)
@@ -597,9 +607,9 @@ class _BedrockConverseProviderRequest(_BedrockProviderRequest):
597
607
  def process_stop_reason(self, reason: str) -> None:
598
608
  if reason == "guardrail_intervened":
599
609
  # record both as a semantic failure and guardrail action so it is discoverable through both properties
600
- self.add_internal_request_property('system.failure', reason)
601
- self.add_internal_request_property('system.failure.description', GUARDRAIL_SEMANTIC_FAILURE_DESCRIPTION)
602
- self.add_internal_request_property(GUARDRAIL_ACTION, reason)
610
+ self.add_internal_request_property(PayiPropertyNames.failure, reason)
611
+ self.add_internal_request_property(PayiPropertyNames.failure_description, GUARDRAIL_SEMANTIC_FAILURE_DESCRIPTION)
612
+ self.add_internal_request_property(PayiPropertyNames.aws_bedrock_guardrail_action, reason)
603
613
 
604
614
  def bedrock_converse_process_streaming_for_function_call(request: _ProviderRequest, chunk: 'dict[str, Any]') -> None:
605
615
  contentBlockStart = chunk.get("contentBlockStart", {})
payi/lib/helpers.py CHANGED
@@ -25,6 +25,18 @@ class PayiCategories:
25
25
  aws_bedrock:str = "system.aws.bedrock"
26
26
  google_vertex:str = "system.google.vertex"
27
27
 
28
+ class PayiPropertyNames:
29
+ failure:str = "system.failure"
30
+ failure_description:str = "system.failure.description"
31
+
32
+ account_name:str = "system.account_name"
33
+ use_case_step:str = "system.use_case_step"
34
+ user_id:str = "system.user_id"
35
+
36
+ aws_bedrock_guardrail_id:str = "system.aws.bedrock.guardrail.id"
37
+ aws_bedrock_guardrail_version:str = "system.aws.bedrock.guardrail.version"
38
+ aws_bedrock_guardrail_action:str = "system.aws.bedrock.guardrail.action"
39
+
28
40
  def create_limit_header_from_ids(*, limit_ids: List[str]) -> Dict[str, str]:
29
41
  if not isinstance(limit_ids, list): # type: ignore
30
42
  raise TypeError("limit_ids must be a list")
payi/lib/instrument.py CHANGED
@@ -2,13 +2,14 @@ import os
2
2
  import json
3
3
  import time
4
4
  import uuid
5
+ import atexit
5
6
  import asyncio
6
7
  import inspect
7
8
  import logging
8
9
  import traceback
9
10
  from abc import abstractmethod
10
11
  from enum import Enum
11
- from typing import Any, Set, Union, Callable, Optional, Sequence, TypedDict
12
+ from typing import Any, Set, Union, Optional, Sequence, TypedDict
12
13
  from datetime import datetime, timezone
13
14
  from dataclasses import dataclass
14
15
 
@@ -17,7 +18,7 @@ from wrapt import ObjectProxy # type: ignore
17
18
 
18
19
  from payi import Payi, AsyncPayi, APIStatusError, APIConnectionError, __version__ as _payi_version
19
20
  from payi.types import IngestUnitsParams
20
- from payi.lib.helpers import PayiHeaderNames
21
+ from payi.lib.helpers import PayiHeaderNames, PayiPropertyNames
21
22
  from payi.types.shared import XproxyResult
22
23
  from payi.types.ingest_response import IngestResponse
23
24
  from payi.types.ingest_units_params import Units, ProviderResponseFunctionCall
@@ -117,10 +118,10 @@ class _ProviderRequest:
117
118
  except Exception as _ex:
118
119
  pass
119
120
 
120
- self.add_internal_request_property('system.failure', exception_str)
121
+ self.add_internal_request_property(PayiPropertyNames.failure, exception_str)
121
122
  if fields:
122
123
  failure_description = ",".join(fields)
123
- self.add_internal_request_property("system.failure.description", failure_description)
124
+ self.add_internal_request_property(PayiPropertyNames.failure_description, failure_description)
124
125
 
125
126
  if "http_status_code" not in self._ingest:
126
127
  # use a non existent http status code so when presented to the user, the origin is clear
@@ -148,6 +149,9 @@ class _ProviderRequest:
148
149
  class PayiInstrumentAwsBedrockConfig(TypedDict, total=False):
149
150
  guardrail_trace: bool
150
151
 
152
+ class PayiInstrumentOfflineInstrumentationConfig(TypedDict, total=False):
153
+ file_name: str
154
+
151
155
  class PayiInstrumentConfig(TypedDict, total=False):
152
156
  proxy: bool
153
157
  global_instrumentation: bool
@@ -163,6 +167,7 @@ class PayiInstrumentConfig(TypedDict, total=False):
163
167
  request_tags: Optional["list[str]"]
164
168
  request_properties: Optional["dict[str, str]"]
165
169
  aws_config: Optional[PayiInstrumentAwsBedrockConfig]
170
+ offline_instrumentation: Optional[PayiInstrumentOfflineInstrumentationConfig]
166
171
 
167
172
  class PayiContext(TypedDict, total=False):
168
173
  use_case_name: Optional[str]
@@ -190,7 +195,6 @@ class _Context(TypedDict, total=False):
190
195
  limit_ids: Optional['list[str]']
191
196
  user_id: Optional[str]
192
197
  account_name: Optional[str]
193
- request_tags: Optional["list[str]"]
194
198
  request_properties: Optional["dict[str, str]"]
195
199
  price_as_category: Optional[str]
196
200
  price_as_resource: Optional[str]
@@ -236,9 +240,6 @@ class _PayiInstrumentor:
236
240
  instruments: Union[Set[str], None] = None,
237
241
  log_prompt_and_response: bool = True,
238
242
  logger: Optional[logging.Logger] = None,
239
- prompt_and_response_logger: Optional[
240
- Callable[[str, "dict[str, str]"], None]
241
- ] = None, # (request id, dict of data to store) -> None
242
243
  global_config: PayiInstrumentConfig = {},
243
244
  caller_filename: str = ""
244
245
  ):
@@ -257,7 +258,6 @@ class _PayiInstrumentor:
257
258
 
258
259
  self._context_stack: list[_Context] = [] # Stack of context dictionaries
259
260
  self._log_prompt_and_response: bool = log_prompt_and_response
260
- self._prompt_and_response_logger: Optional[Callable[[str, dict[str, str]], None]] = prompt_and_response_logger
261
261
 
262
262
  self._blocked_limits: set[str] = set()
263
263
  self._exceeded_limits: set[str] = set()
@@ -275,6 +275,17 @@ class _PayiInstrumentor:
275
275
 
276
276
  self._last_result: Optional[Union[XproxyResult, XproxyError]] = None
277
277
 
278
+ self._offline_instrumentation = global_config.pop("offline_instrumentation", None)
279
+ self._offline_ingest_packets: list[IngestUnitsParams] = []
280
+ self._offline_instrumentation_file_name: Optional[str] = None
281
+
282
+ if self._offline_instrumentation is not None:
283
+ timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
284
+ self._offline_instrumentation_file_name = self._offline_instrumentation.get("file_name", f"payi_instrumentation_{timestamp}.json")
285
+
286
+ # Register exit handler to write packets when process exits
287
+ atexit.register(lambda: self._write_offline_ingest_packets())
288
+
278
289
  global_instrumentation = global_config.pop("global_instrumentation", True)
279
290
 
280
291
  if instruments is None or "*" in instruments:
@@ -287,9 +298,7 @@ class _PayiInstrumentor:
287
298
  global_config["proxy"] = self._proxy_default
288
299
 
289
300
  # Use default clients if not provided for global ingest instrumentation
290
- if not self._payi and not self._apayi:
291
- self._payi = Payi()
292
- self._apayi = AsyncPayi()
301
+ self._ensure_payi_clients()
293
302
 
294
303
  if "use_case_name" not in global_config and caller_filename:
295
304
  description = f"Default use case for {caller_filename}.py"
@@ -298,6 +307,9 @@ class _PayiInstrumentor:
298
307
  self._payi.use_cases.definitions.create(name=caller_filename, description=description)
299
308
  elif self._apayi:
300
309
  self._call_async_use_case_definition_create(use_case_name=caller_filename, use_case_description=description)
310
+ else:
311
+ # in the case of _local_instrumentation is not None
312
+ pass
301
313
  global_config["use_case_name"] = caller_filename
302
314
  except Exception as e:
303
315
  self._logger.error(f"Error creating default use case definition based on file name {caller_filename}: {e}")
@@ -315,6 +327,14 @@ class _PayiInstrumentor:
315
327
 
316
328
  self._init_current_context(**context)
317
329
 
330
+ def _ensure_payi_clients(self) -> None:
331
+ if self._offline_instrumentation is not None:
332
+ return
333
+
334
+ if not self._payi and not self._apayi:
335
+ self._payi = Payi()
336
+ self._apayi = AsyncPayi()
337
+
318
338
  def _instrument_all(self, global_config: PayiInstrumentConfig) -> None:
319
339
  self._instrument_openai()
320
340
  self._instrument_anthropic()
@@ -378,6 +398,30 @@ class _PayiInstrumentor:
378
398
  except Exception as e:
379
399
  self._logger.error(f"Error instrumenting Google GenAi: {e}")
380
400
 
401
+ def _write_offline_ingest_packets(self) -> None:
402
+ if not self._offline_instrumentation_file_name or not self._offline_ingest_packets:
403
+ return
404
+
405
+ try:
406
+ # Convert datetime objects to ISO strings for JSON serialization
407
+ serializable_packets: list[IngestUnitsParams] = []
408
+ for packet in self._offline_ingest_packets:
409
+ serializable_packet = packet.copy()
410
+
411
+ # Convert datetime fields to ISO format strings
412
+ if 'event_timestamp' in serializable_packet and isinstance(serializable_packet['event_timestamp'], datetime):
413
+ serializable_packet['event_timestamp'] = serializable_packet['event_timestamp'].isoformat()
414
+
415
+ serializable_packets.append(serializable_packet)
416
+
417
+ with open(self._offline_instrumentation_file_name, 'w', encoding='utf-8') as f:
418
+ json.dump(serializable_packets, f)
419
+
420
+ self._logger.debug(f"Written {len(self._offline_ingest_packets)} ingest packets to {self._offline_instrumentation_file_name}")
421
+
422
+ except Exception as e:
423
+ self._logger.error(f"Error writing offline ingest packets to {self._offline_instrumentation_file_name}: {e}")
424
+
381
425
  @staticmethod
382
426
  def _create_logged_ingest_units(
383
427
  ingest_units: IngestUnitsParams,
@@ -397,7 +441,6 @@ class _PayiInstrumentor:
397
441
  def _process_ingest_units(
398
442
  self,
399
443
  request: _ProviderRequest,
400
- log_data: 'dict[str, str]',
401
444
  extra_headers: 'dict[str, str]') -> None:
402
445
  ingest_units = request._ingest
403
446
 
@@ -408,6 +451,9 @@ class _PayiInstrumentor:
408
451
  # convert the function call builder to a list of function calls
409
452
  ingest_units["provider_response_function_calls"] = list(request._function_call_builder.values())
410
453
 
454
+ if "provider_response_id" not in ingest_units or not ingest_units["provider_response_id"]:
455
+ ingest_units["provider_response_id"] = f"payi_{uuid.uuid4()}"
456
+
411
457
  if 'resource' not in ingest_units or ingest_units['resource'] == '':
412
458
  ingest_units['resource'] = "system.unknown_model"
413
459
 
@@ -435,19 +481,6 @@ class _PayiInstrumentor:
435
481
  if not units or all(unit.get("input", 0) == 0 and unit.get("output", 0) == 0 for unit in units.values()):
436
482
  self._logger.info('ingesting with no token counts')
437
483
 
438
- if self._log_prompt_and_response and self._prompt_and_response_logger:
439
- response_json = ingest_units.pop("provider_response_json", None)
440
- request_json = ingest_units.pop("provider_request_json", None)
441
- stack_trace = ingest_units.get("properties", {}).pop("system.stack_trace", None) # type: ignore
442
-
443
- if response_json is not None:
444
- # response_json is a list of strings, convert a single json string
445
- log_data["provider_response_json"] = json.dumps(response_json)
446
- if request_json is not None:
447
- log_data["provider_request_json"] = request_json
448
- if stack_trace is not None:
449
- log_data["stack_trace"] = stack_trace
450
-
451
484
  def _process_ingest_units_response(self, ingest_response: IngestResponse) -> None:
452
485
  if ingest_response.xproxy_result.limits:
453
486
  for limit_id, state in ingest_response.xproxy_result.limits.items():
@@ -490,11 +523,8 @@ class _PayiInstrumentor:
490
523
 
491
524
  self._logger.debug(f"_aingest_units")
492
525
 
493
- # return early if there are no units to ingest and on a successul ingest request
494
- log_data: 'dict[str,str]' = {}
495
526
  extra_headers: 'dict[str, str]' = {}
496
-
497
- self._process_ingest_units(request, log_data=log_data, extra_headers=extra_headers)
527
+ self._process_ingest_units(request, extra_headers=extra_headers)
498
528
 
499
529
  try:
500
530
  if self._logger.isEnabledFor(logging.DEBUG):
@@ -504,6 +534,18 @@ class _PayiInstrumentor:
504
534
  ingest_response = await self._apayi.ingest.units(**ingest_units, extra_headers=extra_headers)
505
535
  elif self._payi:
506
536
  ingest_response = self._payi.ingest.units(**ingest_units, extra_headers=extra_headers)
537
+ elif self._offline_instrumentation is not None:
538
+ self._offline_ingest_packets.append(ingest_units.copy())
539
+
540
+ # simulate a successful ingest for local instrumentation
541
+ now=datetime.now(timezone.utc)
542
+ ingest_response = IngestResponse(
543
+ event_timestamp=now,
544
+ ingest_timestamp=now,
545
+ request_id="local_instrumentation",
546
+ xproxy_result=XproxyResult(request_id="local_instrumentation"))
547
+ pass
548
+
507
549
  else:
508
550
  self._logger.error("No payi instance to ingest units")
509
551
  return XproxyError(code="configuration_error", message="No Payi or AsyncPayi instance configured for ingesting units")
@@ -513,10 +555,6 @@ class _PayiInstrumentor:
513
555
  if ingest_response:
514
556
  self._process_ingest_units_response(ingest_response)
515
557
 
516
- if ingest_response and self._log_prompt_and_response and self._prompt_and_response_logger:
517
- request_id = ingest_response.xproxy_result.request_id
518
- self._prompt_and_response_logger(request_id, log_data) # type: ignore
519
-
520
558
  return ingest_response.xproxy_result
521
559
 
522
560
  except APIConnectionError as api_ex:
@@ -609,10 +647,8 @@ class _PayiInstrumentor:
609
647
 
610
648
  self._logger.debug(f"_ingest_units")
611
649
 
612
- # return early if there are no units to ingest and on a successul ingest request
613
- log_data: 'dict[str,str]' = {}
614
650
  extra_headers: 'dict[str, str]' = {}
615
- self._process_ingest_units(request, log_data=log_data, extra_headers=extra_headers)
651
+ self._process_ingest_units(request, extra_headers=extra_headers)
616
652
 
617
653
  try:
618
654
  if self._payi:
@@ -624,16 +660,18 @@ class _PayiInstrumentor:
624
660
 
625
661
  self._process_ingest_units_response(ingest_response)
626
662
 
627
- if self._log_prompt_and_response and self._prompt_and_response_logger:
628
- request_id = ingest_response.xproxy_result.request_id
629
- self._prompt_and_response_logger(request_id, log_data) # type: ignore
630
-
631
663
  return ingest_response.xproxy_result
632
664
  elif self._apayi:
633
665
  # task runs async. aingest_units will invoke the callback and post process
634
666
  sync_response = self._call_aingest_sync(request)
635
667
  self._logger.debug(f"_ingest_units: apayi success ({sync_response})")
636
668
  return sync_response
669
+ elif self._offline_instrumentation is not None:
670
+ self._offline_ingest_packets.append(ingest_units.copy())
671
+
672
+ # simulate a successful ingest for local instrumentation
673
+ return XproxyResult(request_id="local_instrumentation")
674
+
637
675
  else:
638
676
  self._logger.error("No payi instance to ingest units")
639
677
  return XproxyError(code="configuration_error", message="No Payi or AsyncPayi instance configured for ingesting units")
@@ -661,6 +699,31 @@ class _PayiInstrumentor:
661
699
 
662
700
  return {}
663
701
 
702
+ @staticmethod
703
+ def _valid_str_or_none(value: Optional[str], default: Optional[str] = None) -> Optional[str]:
704
+ if value is None:
705
+ return default
706
+ elif len(value) == 0:
707
+ # an empty string explicitly blocks the default value
708
+ return None
709
+ else:
710
+ return value
711
+
712
+ @staticmethod
713
+ def _valid_properties_or_none(value: Optional["dict[str, str]"], default: Optional["dict[str, str]"] = None) -> Optional["dict[str, str]"]:
714
+ if value is None:
715
+ return default.copy() if default else None
716
+ elif len(value) == 0:
717
+ # an empty dictionary explicitly blocks the default value
718
+ return None
719
+ elif default:
720
+ # merge dictionaries, child overrides parent keys
721
+ merged = default.copy()
722
+ merged.update(value)
723
+ return merged
724
+ else:
725
+ return value.copy()
726
+
664
727
  def _init_current_context(
665
728
  self,
666
729
  proxy: Optional[bool] = None,
@@ -671,7 +734,6 @@ class _PayiInstrumentor:
671
734
  use_case_step: Optional[str]= None,
672
735
  user_id: Optional[str]= None,
673
736
  account_name: Optional[str]= None,
674
- request_tags: Optional["list[str]"] = None,
675
737
  request_properties: Optional["dict[str, str]"] = None,
676
738
  use_case_properties: Optional["dict[str, str]"] = None,
677
739
  price_as_category: Optional[str] = None,
@@ -689,7 +751,6 @@ class _PayiInstrumentor:
689
751
  parent_use_case_name = parent_context.get("use_case_name", None)
690
752
  parent_use_case_id = parent_context.get("use_case_id", None)
691
753
  parent_use_case_version = parent_context.get("use_case_version", None)
692
- parent_use_case_step = parent_context.get("use_case_step", None)
693
754
 
694
755
  assign_use_case_values = False
695
756
 
@@ -721,26 +782,12 @@ class _PayiInstrumentor:
721
782
  assign_use_case_values = True
722
783
 
723
784
  if assign_use_case_values:
724
- context["use_case_id"] = use_case_id if use_case_id else parent_use_case_id
725
- context["use_case_version"] = use_case_version if use_case_version else parent_use_case_version
726
- context["use_case_step"] = use_case_step if use_case_step else parent_use_case_step
785
+ context["use_case_version"] = use_case_version if use_case_version is not None else parent_use_case_version
786
+ context["use_case_id"] = self._valid_str_or_none(use_case_id, parent_use_case_id)
787
+ context["use_case_step"] = self._valid_str_or_none(use_case_step, None)
727
788
 
728
789
  parent_use_case_properties = parent_context.get("use_case_properties", None)
729
- if use_case_properties is not None:
730
- if not use_case_properties:
731
- # an empty dictionary explicitly blocks inheriting from the parent state
732
- context["use_case_properties"] = None
733
- else:
734
- if parent_use_case_properties:
735
- # merge dictionaries, child overrides parent keys
736
- merged = parent_use_case_properties.copy()
737
- merged.update(use_case_properties)
738
- context["use_case_properties"] = merged
739
- else:
740
- context["use_case_properties"] = use_case_properties.copy()
741
- elif parent_use_case_properties:
742
- # use the parent use_case_properties if it exists
743
- context["use_case_properties"] = parent_use_case_properties.copy()
790
+ context["use_case_properties"] = self._valid_properties_or_none(use_case_properties, parent_use_case_properties)
744
791
 
745
792
  parent_limit_ids = parent_context.get("limit_ids", None)
746
793
  if limit_ids is None:
@@ -754,56 +801,13 @@ class _PayiInstrumentor:
754
801
  context["limit_ids"] = list(set(limit_ids) | set(parent_limit_ids)) if parent_limit_ids else limit_ids.copy()
755
802
 
756
803
  parent_user_id = parent_context.get("user_id", None)
757
- if user_id is None:
758
- # use the parent user_id if it exists
759
- context["user_id"] = parent_user_id
760
- elif len(user_id) == 0:
761
- # caller passing an empty string explicitly blocks inheriting from the parent state
762
- context["user_id"] = None
763
- else:
764
- context["user_id"] = user_id
804
+ context["user_id"] = self._valid_str_or_none(user_id, parent_user_id)
765
805
 
766
806
  parent_account_name = parent_context.get("account_name", None)
767
- if account_name is None:
768
- # use the parent account_name if it exists
769
- context["account_name"] = parent_account_name
770
- elif len(account_name) == 0:
771
- # caller passing an empty string explicitly blocks inheriting from the parent state
772
- context["account_name"] = None
773
- else:
774
- context["account_name"] = account_name
775
-
776
- parent_request_tags = parent_context.get("request_tags", None)
777
- if request_tags is not None:
778
- if len(request_tags) == 0:
779
- # caller passing an empty list explicitly blocks inheriting from the parent state
780
- context["request_tags"] = None
781
- else:
782
- if parent_request_tags:
783
- # union of new and parent lists if the parent context contains request tags
784
- context["request_tags"] = list(set(request_tags) | set(parent_request_tags))
785
- else:
786
- context["request_tags"] = request_tags.copy()
787
- elif parent_request_tags:
788
- # use the parent request_tags if it exists
789
- context["request_tags"] = parent_request_tags.copy()
807
+ context["account_name"] = self._valid_str_or_none(account_name, parent_account_name)
790
808
 
791
809
  parent_request_properties = parent_context.get("request_properties", None)
792
- if request_properties is not None:
793
- if not request_properties:
794
- # an empty dictionary explicitly blocks inheriting from the parent state
795
- context["request_properties"] = None
796
- else:
797
- if parent_request_properties:
798
- # merge dictionaries, child overrides parent keys
799
- merged = parent_request_properties.copy()
800
- merged.update(request_properties)
801
- context["request_properties"] = merged
802
- else:
803
- context["request_properties"] = request_properties.copy()
804
- elif parent_request_properties:
805
- # use the parent request_properties if it exists
806
- context["request_properties"] = parent_request_properties.copy()
810
+ context["request_properties"] = self._valid_properties_or_none(request_properties, parent_request_properties)
807
811
 
808
812
  if price_as_category:
809
813
  context["price_as_category"] = price_as_category
@@ -822,7 +826,6 @@ class _PayiInstrumentor:
822
826
  use_case_version: Optional[int],
823
827
  user_id: Optional[str],
824
828
  account_name: Optional[str],
825
- request_tags: Optional["list[str]"] = None,
826
829
  request_properties: Optional["dict[str, str]"] = None,
827
830
  use_case_properties: Optional["dict[str, str]"] = None,
828
831
  *args: Any,
@@ -837,7 +840,6 @@ class _PayiInstrumentor:
837
840
  use_case_version=use_case_version,
838
841
  user_id=user_id,
839
842
  account_name=account_name,
840
- request_tags=request_tags,
841
843
  request_properties=request_properties,
842
844
  use_case_properties=use_case_properties
843
845
  )
@@ -853,7 +855,6 @@ class _PayiInstrumentor:
853
855
  use_case_version: Optional[int],
854
856
  user_id: Optional[str],
855
857
  account_name: Optional[str],
856
- request_tags: Optional["list[str]"] = None,
857
858
  request_properties: Optional["dict[str, str]"] = None,
858
859
  use_case_properties: Optional["dict[str, str]"] = None,
859
860
  *args: Any,
@@ -868,7 +869,6 @@ class _PayiInstrumentor:
868
869
  use_case_version=use_case_version,
869
870
  user_id=user_id,
870
871
  account_name=account_name,
871
- request_tags=request_tags,
872
872
  request_properties=request_properties,
873
873
  use_case_properties=use_case_properties)
874
874
  return func(*args, **kwargs)
@@ -901,7 +901,8 @@ class _PayiInstrumentor:
901
901
  ) -> None:
902
902
 
903
903
  limit_ids = ingest_extra_headers.pop(PayiHeaderNames.limit_ids, None)
904
- request_tags = ingest_extra_headers.pop(PayiHeaderNames.request_tags, None)
904
+ # pop and ignore the request tags header since it is no longer processed
905
+ ingest_extra_headers.pop(PayiHeaderNames.request_tags, None)
905
906
 
906
907
  use_case_name = ingest_extra_headers.pop(PayiHeaderNames.use_case_name, None)
907
908
  use_case_id = ingest_extra_headers.pop(PayiHeaderNames.use_case_id, None)
@@ -913,8 +914,6 @@ class _PayiInstrumentor:
913
914
 
914
915
  if limit_ids:
915
916
  request._ingest["limit_ids"] = limit_ids.split(",")
916
- if request_tags:
917
- request._ingest["request_tags"] = request_tags.split(",")
918
917
  if use_case_name:
919
918
  request._ingest["use_case_name"] = use_case_name
920
919
  if use_case_id:
@@ -1259,7 +1258,6 @@ class _PayiInstrumentor:
1259
1258
 
1260
1259
  context_user_id: Optional[str] = context.get("user_id")
1261
1260
  context_account_name: Optional[str] = context.get("account_name")
1262
- context_request_tags: Optional[list[str]] = context.get("request_tags")
1263
1261
 
1264
1262
  context_price_as_category: Optional[str] = context.get("price_as_category")
1265
1263
  context_price_as_resource: Optional[str] = context.get("price_as_resource")
@@ -1320,10 +1318,7 @@ class _PayiInstrumentor:
1320
1318
  if context_use_case_version is not None:
1321
1319
  extra_headers[PayiHeaderNames.use_case_version] = str(context_use_case_version)
1322
1320
  if context_use_case_step is not None:
1323
- extra_headers[PayiHeaderNames.use_case_step] = str(context_use_case_step)
1324
-
1325
- if PayiHeaderNames.request_tags not in extra_headers and context_request_tags:
1326
- extra_headers[PayiHeaderNames.request_tags] = ",".join(context_request_tags)
1321
+ extra_headers[PayiHeaderNames.use_case_step] = context_use_case_step
1327
1322
 
1328
1323
  if PayiHeaderNames.price_as_category not in extra_headers and context_price_as_category:
1329
1324
  extra_headers[PayiHeaderNames.price_as_category] = context_price_as_category
@@ -1730,7 +1725,6 @@ def payi_instrument(
1730
1725
  payi: Optional[Union[Payi, AsyncPayi, 'list[Union[Payi, AsyncPayi]]']] = None,
1731
1726
  instruments: Optional[Set[str]] = None,
1732
1727
  log_prompt_and_response: bool = True,
1733
- prompt_and_response_logger: Optional[Callable[[str, "dict[str, str]"], None]] = None,
1734
1728
  config: Optional[PayiInstrumentConfig] = None,
1735
1729
  logger: Optional[logging.Logger] = None,
1736
1730
  ) -> None:
@@ -1763,7 +1757,6 @@ def payi_instrument(
1763
1757
  instruments=instruments,
1764
1758
  log_prompt_and_response=log_prompt_and_response,
1765
1759
  logger=logger,
1766
- prompt_and_response_logger=prompt_and_response_logger,
1767
1760
  global_config=config if config else PayiInstrumentConfig(),
1768
1761
  caller_filename=caller_filename
1769
1762
  )
@@ -1781,6 +1774,7 @@ def track(
1781
1774
  use_case_properties: Optional["dict[str, str]"] = None,
1782
1775
  proxy: Optional[bool] = None,
1783
1776
  ) -> Any:
1777
+ _ = request_tags
1784
1778
 
1785
1779
  def _track(func: Any) -> Any:
1786
1780
  import asyncio
@@ -1801,7 +1795,6 @@ def track(
1801
1795
  use_case_version,
1802
1796
  user_id,
1803
1797
  account_name,
1804
- request_tags,
1805
1798
  request_properties,
1806
1799
  use_case_properties,
1807
1800
  *args,
@@ -1825,7 +1818,6 @@ def track(
1825
1818
  use_case_version,
1826
1819
  user_id,
1827
1820
  account_name,
1828
- request_tags,
1829
1821
  request_properties,
1830
1822
  use_case_properties,
1831
1823
  *args,
@@ -1865,7 +1857,6 @@ def track_context(
1865
1857
 
1866
1858
  context["user_id"] = user_id
1867
1859
  context["account_name"] = account_name
1868
- context["request_tags"] = request_tags
1869
1860
 
1870
1861
  context["price_as_category"] = price_as_category
1871
1862
  context["price_as_resource"] = price_as_resource
@@ -1874,6 +1865,8 @@ def track_context(
1874
1865
  context["request_properties"] = request_properties
1875
1866
  context["use_case_properties"] = use_case_properties
1876
1867
 
1868
+ _ = request_tags
1869
+
1877
1870
  return _InternalTrackContext(context)
1878
1871
 
1879
1872
  def get_context() -> PayiContext:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: payi
3
- Version: 0.1.0a125
3
+ Version: 0.1.0a126
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=Z_wIyo206T6Jqh2rolFg2VXZgX24PahLmpURp0-NssU,10092
13
13
  payi/_types.py,sha256=d6xrZDG6rG6opphTN7UVYdEOis3977LrQQgpNtklXZE,7234
14
- payi/_version.py,sha256=WF7cqMNU2-Gx3OUMUfQ024I9Rxc7ZM51YSNr9BaAkyg,166
14
+ payi/_version.py,sha256=Q_d5ojHOSTeTtbTlwPmPpab5qD4yxOTyShC1FSrNwdk,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
@@ -28,14 +28,14 @@ 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
30
  payi/lib/AnthropicInstrumentor.py,sha256=SI6nzU-eVufHbxV5gxgrZDhx3TewRVnsvtwDuwEG6WU,16484
31
- payi/lib/BedrockInstrumentor.py,sha256=sbjq5GKBOjjId1cj4x0vHQIGc_JBxewSE08cgvyAD3E,26441
31
+ payi/lib/BedrockInstrumentor.py,sha256=-dF3mk2nsCOrYxeV75eBORxYEJ7Nd1nIJWsy4-2LEHk,27324
32
32
  payi/lib/GoogleGenAiInstrumentor.py,sha256=LHiEZ7G5IhCcDlpVzQlXW9Ok96MHLeq7emEhFzPBTm0,8836
33
33
  payi/lib/OpenAIInstrumentor.py,sha256=_ULwIli11XP1yZK_pMGXuaSmHZ5pozuEt_v5DfhNuGw,22914
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=jcMyxsuWmyPymDCYmDbQAb6IgbkmkiiNUaxeEPkCKZs,4457
38
- payi/lib/instrument.py,sha256=LX6EqfEGGzIBJKUm0tQDh7ianXzKkbicZltYkHyqTUk,77590
37
+ payi/lib/helpers.py,sha256=6c0RFMS0AYVIxU6q8ak1CDwMTwldIN7N2O2XkxTO7ag,4931
38
+ payi/lib/instrument.py,sha256=eaDtiBx3cuCOB4Pxd1svEOEk6MykB_Dn3JaCJKl8Z6w,76473
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
@@ -142,7 +142,7 @@ payi/types/use_cases/definitions/kpi_retrieve_response.py,sha256=uQXliSvS3k-yDYw
142
142
  payi/types/use_cases/definitions/kpi_update_params.py,sha256=jbawdWAdMnsTWVH0qfQGb8W7_TXe3lq4zjSRu44d8p8,373
143
143
  payi/types/use_cases/definitions/kpi_update_response.py,sha256=zLyEoT0S8d7XHsnXZYT8tM7yDw0Aze0Mk-_Z6QeMtc8,459
144
144
  payi/types/use_cases/definitions/limit_config_create_params.py,sha256=sodtLT84tBmuO_0d-h0CZWCh4vWojJMtUbMjBmEN3IE,492
145
- payi-0.1.0a125.dist-info/METADATA,sha256=6lZARC9PPe0z9KjpplAqnFLHrWnwn7NqMka9_cSyYWo,16324
146
- payi-0.1.0a125.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
147
- payi-0.1.0a125.dist-info/licenses/LICENSE,sha256=CQt03aM-P4a3Yg5qBg3JSLVoQS3smMyvx7tYg_6V7Gk,11334
148
- payi-0.1.0a125.dist-info/RECORD,,
145
+ payi-0.1.0a126.dist-info/METADATA,sha256=HMMB7v-Q7WLRb5x3j2T4MUUB-dI5WtH6Ne0_J1w0ukQ,16324
146
+ payi-0.1.0a126.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
147
+ payi-0.1.0a126.dist-info/licenses/LICENSE,sha256=CQt03aM-P4a3Yg5qBg3JSLVoQS3smMyvx7tYg_6V7Gk,11334
148
+ payi-0.1.0a126.dist-info/RECORD,,