payi 0.1.0a63__py3-none-any.whl → 0.1.0a65__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/lib/instrument.py CHANGED
@@ -21,10 +21,25 @@ from .helpers import PayiCategories
21
21
  from .Stopwatch import Stopwatch
22
22
 
23
23
 
24
+ class _ProviderRequest:
25
+ def __init__(self, instrumentor: '_PayiInstrumentor'):
26
+ self._instrumentor: '_PayiInstrumentor' = instrumentor
27
+ self._estimated_prompt_tokens: Optional[int] = None
28
+ self._ingest: IngestUnitsParams
29
+
30
+ def process_request(self, _kwargs: Any) -> None:
31
+ return
32
+
33
+ def process_chunk(self, _chunk: Any) -> bool:
34
+ return True
35
+
36
+ def process_synchronous_response(self, response: Any, log_prompt_and_response: bool, kwargs: Any) -> Optional[object]: # noqa: ARG002
37
+ return None
38
+
24
39
  class PayiInstrumentConfig(TypedDict, total=False):
25
40
  proxy: bool
41
+ global_instrumentation_enabled: bool
26
42
  limit_ids: Optional["list[str]"]
27
- request_tags: Optional["list[str]"]
28
43
  experience_name: Optional[str]
29
44
  experience_id: Optional[str]
30
45
  use_case_name: Optional[str]
@@ -40,26 +55,14 @@ class _Context(TypedDict, total=False):
40
55
  use_case_id: Optional[str]
41
56
  use_case_version: Optional[int]
42
57
  limit_ids: Optional['list[str]']
43
- request_tags: Optional['list[str]']
44
58
  user_id: Optional[str]
45
59
 
46
- class _ParentState(TypedDict, total=False):
47
- experience_name: Optional[str]
48
- experience_id: Optional[str]
49
- use_case_name: Optional[str]
50
- use_case_id: Optional[str]
51
- use_case_version: Optional[int]
52
- limit_ids: Optional['list[str]']
53
- request_tags: Optional['list[str]']
54
-
55
60
  class _IsStreaming(Enum):
56
61
  false = 0
57
62
  true = 1
58
63
  kwargs = 2
59
64
 
60
65
  class _PayiInstrumentor:
61
- estimated_prompt_tokens: str = "estimated_prompt_tokens"
62
-
63
66
  def __init__(
64
67
  self,
65
68
  payi: Optional[Payi],
@@ -86,11 +89,23 @@ class _PayiInstrumentor:
86
89
  else:
87
90
  self._instrument_specific(instruments)
88
91
 
89
- if global_config and len(global_config) > 0:
92
+ global_instrumentation_enabled = global_config.pop("global_instrumentation_enabled", True) if global_config else True
93
+
94
+ if global_instrumentation_enabled:
95
+ if global_config is None:
96
+ global_config = {}
97
+ if "proxy" not in global_config:
98
+ global_config["proxy"] = False
99
+
100
+ # Use default clients if not provided for global ingest instrumentation
101
+ if not self._payi and not self._apayi and global_config.get("proxy") == False:
102
+ self._payi = Payi()
103
+ self._apayi = AsyncPayi()
104
+
90
105
  context: _Context = {}
91
106
  self._context_stack.append(context)
92
107
  # init_context will update the currrent context stack location
93
- self._init_context(context=context, parentState={}, **global_config)
108
+ self._init_context(context=context, parentContext={}, **global_config) # type: ignore
94
109
 
95
110
  def _instrument_all(self) -> None:
96
111
  self._instrument_openai()
@@ -252,31 +267,24 @@ class _PayiInstrumentor:
252
267
 
253
268
  def _setup_call_func(
254
269
  self
255
- ) -> 'tuple[_Context, _ParentState]':
270
+ ) -> 'tuple[_Context, _Context]':
256
271
  context: _Context = {}
257
- parentState: _ParentState = {}
272
+ parentContext: _Context = {}
258
273
 
259
274
  if len(self._context_stack) > 0:
260
275
  # copy current context into the upcoming context
261
276
  context = self._context_stack[-1].copy()
262
277
  context.pop("proxy")
263
- parentState["experience_name"] = context.get("experience_name", None)
264
- parentState["experience_id"] = context.get("experience_id", None)
265
- parentState["use_case_name"] = context.get("use_case_name", None)
266
- parentState["use_case_id"] = context.get("use_case_id", None)
267
- parentState["use_case_version"] = context.get("use_case_version", None)
268
- parentState["limit_ids"] = context.get("limit_ids", None)
269
- parentState["request_tags"] = context.get("request_tags", None)
278
+ parentContext = {**context}
270
279
 
271
- return (context, parentState)
280
+ return (context, parentContext)
272
281
 
273
282
  def _init_context(
274
283
  self,
275
284
  context: _Context,
276
- parentState: _ParentState,
285
+ parentContext: _Context,
277
286
  proxy: bool,
278
287
  limit_ids: Optional["list[str]"] = None,
279
- request_tags: Optional["list[str]"] = None,
280
288
  experience_name: Optional[str] = None,
281
289
  experience_id: Optional[str] = None,
282
290
  use_case_name: Optional[str]= None,
@@ -286,56 +294,72 @@ class _PayiInstrumentor:
286
294
  ) -> None:
287
295
  context["proxy"] = proxy
288
296
 
289
- # TODO use case what if caller specified epxerience / use_case ID and no name?
297
+ parent_experience_name = parentContext.get("experience_name", None)
298
+ parent_experience_id = parentContext.get("experience_id", None)
290
299
 
291
- # Handle experience name and ID logic
292
- if not experience_name:
300
+ if experience_name is None:
293
301
  # If no experience_name specified, use previous values
294
- context["experience_name"] = parentState.get("experience_name", None)
295
- context["experience_id"] = parentState.get("experience_id", None)
302
+ context["experience_name"] = parent_experience_name
303
+ context["experience_id"] = parent_experience_id
304
+ elif len(experience_name) == 0:
305
+ # Empty string explicitly blocks inheriting from the parent state
306
+ context["experience_name"] = None
307
+ context["experience_id"] = None
296
308
  else:
297
- previous_experience_name = parentState.get("experience_name", None)
298
- previous_experience_id = parentState.get("experience_id", None)
299
-
300
- # If experience_name is specified
301
- if experience_name == previous_experience_name:
309
+ # Check if experience_name is the same as the previous one
310
+ if experience_name == parent_experience_name:
302
311
  # Same experience name, use previous ID unless new one specified
303
312
  context["experience_name"] = experience_name
304
- context["experience_id"] = experience_id if experience_id else previous_experience_id
313
+ context["experience_id"] = experience_id if experience_id else parent_experience_id
305
314
  else:
306
315
  # Different experience name, use specified ID or generate one
307
316
  context["experience_name"] = experience_name
308
317
  context["experience_id"] = experience_id if experience_id else str(uuid.uuid4())
309
318
 
310
- # Handle use case name and ID logic
311
- if not use_case_name: # TODO use case
319
+ parent_use_case_name = parentContext.get("use_case_name", None)
320
+ parent_use_case_id = parentContext.get("use_case_id", None)
321
+ parent_use_case_version = parentContext.get("use_case_version", None)
322
+
323
+ if use_case_name is None:
312
324
  # If no use_case_name specified, use previous values
313
- context["use_case_name"] = parentState.get("use_case_name", None)
314
- context["use_case_id"] = parentState.get("use_case_id", None)
315
- context["use_case_version"] = parentState.get("use_case_version", None)
325
+ context["use_case_name"] = parent_use_case_name
326
+ context["use_case_id"] = parent_use_case_id
327
+ context["use_case_version"] = parent_use_case_version
328
+ elif len(use_case_name) == 0:
329
+ # Empty string explicitly blocks inheriting from the parent state
330
+ context["use_case_name"] = None
331
+ context["use_case_id"] = None
332
+ context["use_case_version"] = None
316
333
  else:
317
- previous_use_case_name = parentState.get("use_case_name", None)
318
- previous_use_case_id = parentState.get("use_case_id", None)
319
- previous_use_case_version = parentState.get("use_case_version", None)
320
-
321
- # If use_case_name is specified
322
- if use_case_name == previous_use_case_name:
334
+ if use_case_name == parent_use_case_name:
323
335
  # Same use case name, use previous ID unless new one specified
324
336
  context["use_case_name"] = use_case_name
325
- context["use_case_id"] = use_case_id if use_case_id else previous_use_case_id
326
- context["use_case_version"] = use_case_version if use_case_version else previous_use_case_version
337
+ context["use_case_id"] = use_case_id if use_case_id else parent_use_case_id
338
+ context["use_case_version"] = use_case_version if use_case_version else parent_use_case_version
327
339
  else:
328
- # Different experience name, use specified ID or generate one
340
+ # Different use case name, use specified ID or generate one
329
341
  context["use_case_name"] = use_case_name
330
342
  context["use_case_id"] = use_case_id if use_case_id else str(uuid.uuid4())
331
- context["use_case_version"] = use_case_version
332
-
333
- # set any values explicitly passed by the caller, otherwise use what is already in the context
334
- if limit_ids:
335
- context["limit_ids"] = limit_ids
336
- if request_tags:
337
- context["request_tags"] = request_tags
338
- if user_id:
343
+ context["use_case_version"] = use_case_version if use_case_version else None
344
+
345
+ parent_limit_ids = parentContext.get("limit_ids", None)
346
+ if limit_ids is None:
347
+ # use the parent limit_ids if it exists
348
+ context["limit_ids"] = parent_limit_ids
349
+ elif len(limit_ids) == 0:
350
+ # caller passing an empty array explicitly blocks inheriting from the parent state
351
+ context["limit_ids"] = None
352
+ else:
353
+ # union of new and parent lists if the parent context contains limit ids
354
+ context["limit_ids"] = list(set(limit_ids) | set(parent_limit_ids)) if parent_limit_ids else limit_ids
355
+
356
+ if user_id is None:
357
+ # use the parent user_id if it exists
358
+ context["user_id"] = parentContext.get("user_id", None)
359
+ elif len(user_id) == 0:
360
+ # caller passing an empty string explicitly blocks inheriting from the parent state
361
+ context["user_id"] = None
362
+ else:
339
363
  context["user_id"] = user_id
340
364
 
341
365
  self.set_context(context)
@@ -345,7 +369,6 @@ class _PayiInstrumentor:
345
369
  func: Any,
346
370
  proxy: bool,
347
371
  limit_ids: Optional["list[str]"],
348
- request_tags: Optional["list[str]"],
349
372
  experience_name: Optional[str],
350
373
  experience_id: Optional[str],
351
374
  use_case_name: Optional[str],
@@ -355,15 +378,14 @@ class _PayiInstrumentor:
355
378
  *args: Any,
356
379
  **kwargs: Any,
357
380
  ) -> Any:
358
- context, parentState = self._setup_call_func()
381
+ context, parentContext = self._setup_call_func()
359
382
 
360
383
  with self:
361
384
  self._init_context(
362
385
  context,
363
- parentState,
386
+ parentContext,
364
387
  proxy,
365
388
  limit_ids,
366
- request_tags,
367
389
  experience_name,
368
390
  experience_id,
369
391
  use_case_name,
@@ -377,7 +399,6 @@ class _PayiInstrumentor:
377
399
  func: Any,
378
400
  proxy: bool,
379
401
  limit_ids: Optional["list[str]"],
380
- request_tags: Optional["list[str]"],
381
402
  experience_name: Optional[str],
382
403
  experience_id: Optional[str],
383
404
  use_case_name: Optional[str],
@@ -387,15 +408,14 @@ class _PayiInstrumentor:
387
408
  *args: Any,
388
409
  **kwargs: Any,
389
410
  ) -> Any:
390
- context, parentState = self._setup_call_func()
411
+ context, parentContext = self._setup_call_func()
391
412
 
392
413
  with self:
393
414
  self._init_context(
394
415
  context,
395
- parentState,
416
+ parentContext,
396
417
  proxy,
397
418
  limit_ids,
398
- request_tags,
399
419
  experience_name,
400
420
  experience_id,
401
421
  use_case_name,
@@ -477,9 +497,7 @@ class _PayiInstrumentor:
477
497
  async def achat_wrapper(
478
498
  self,
479
499
  category: str,
480
- process_chunk: Optional[Callable[[Any, IngestUnitsParams], None]],
481
- process_request: Optional[Callable[[IngestUnitsParams, Any, Any], None]],
482
- process_synchronous_response: Any,
500
+ provider: _ProviderRequest,
483
501
  is_streaming: _IsStreaming,
484
502
  wrapped: Any,
485
503
  instance: Any,
@@ -496,7 +514,7 @@ class _PayiInstrumentor:
496
514
 
497
515
  # after _udpate_headers, all metadata to add to ingest is in extra_headers, keyed by the xproxy-xxx header name
498
516
  extra_headers = kwargs.get("extra_headers", {})
499
- self._update_headers(context, extra_headers)
517
+ self._update_extra_headers(context, extra_headers)
500
518
 
501
519
  if context.get("proxy", True):
502
520
  if "extra_headers" not in kwargs:
@@ -504,8 +522,8 @@ class _PayiInstrumentor:
504
522
 
505
523
  return await wrapped(*args, **kwargs)
506
524
 
507
- ingest: IngestUnitsParams = {"category": category, "units": {}} # type: ignore
508
- ingest["resource"] = kwargs.get("model", "")
525
+ provider._ingest = {"category": category, "units": {}} # type: ignore
526
+ provider._ingest["resource"] = kwargs.get("model", "")
509
527
 
510
528
  if category == PayiCategories.openai and instance and hasattr(instance, "_client"):
511
529
  from .OpenAIInstrumentor import OpenAiInstrumentor # noqa: I001
@@ -523,21 +541,20 @@ class _PayiInstrumentor:
523
541
  logging.error("Azure OpenAI invalid resource scope, not ingesting")
524
542
  return wrapped(*args, **kwargs)
525
543
 
526
- ingest["resource_scope"] = resource_scope
544
+ provider._ingest["resource_scope"] = resource_scope
527
545
 
528
546
  category = PayiCategories.azure_openai
529
547
 
530
- ingest["category"] = category
531
- ingest["resource"] = route_as_resource
548
+ provider._ingest["category"] = category
549
+ provider._ingest["resource"] = route_as_resource
532
550
 
533
551
  current_frame = inspect.currentframe()
534
552
  # f_back excludes the current frame, strip() cleans up whitespace and newlines
535
553
  stack = [frame.strip() for frame in traceback.format_stack(current_frame.f_back)] # type: ignore
536
554
 
537
- ingest['properties'] = { 'system.stack_trace': json.dumps(stack) }
555
+ provider._ingest['properties'] = { 'system.stack_trace': json.dumps(stack) }
538
556
 
539
- if process_request:
540
- process_request(ingest, (), instance)
557
+ provider.process_request(kwargs)
541
558
 
542
559
  sw = Stopwatch()
543
560
  stream: bool = False
@@ -550,7 +567,7 @@ class _PayiInstrumentor:
550
567
  stream = False
551
568
 
552
569
  try:
553
- self._prepare_ingest(ingest, extra_headers, **kwargs)
570
+ self._prepare_ingest(provider._ingest, extra_headers, **kwargs)
554
571
  sw.start()
555
572
  response = await wrapped(*args, **kwargs)
556
573
 
@@ -568,9 +585,8 @@ class _PayiInstrumentor:
568
585
  instance=instance,
569
586
  instrumentor=self,
570
587
  log_prompt_and_response=self._log_prompt_and_response,
571
- ingest=ingest,
572
588
  stopwatch=sw,
573
- process_chunk=process_chunk,
589
+ provider=provider,
574
590
  is_bedrock=False,
575
591
  )
576
592
 
@@ -578,28 +594,25 @@ class _PayiInstrumentor:
578
594
 
579
595
  sw.stop()
580
596
  duration = sw.elapsed_ms_int()
581
- ingest["end_to_end_latency_ms"] = duration
582
- ingest["http_status_code"] = 200
597
+ provider._ingest["end_to_end_latency_ms"] = duration
598
+ provider._ingest["http_status_code"] = 200
583
599
 
584
- if process_synchronous_response:
585
- return_result: Any = process_synchronous_response(
586
- response=response,
587
- ingest=ingest,
588
- log_prompt_and_response=self._log_prompt_and_response,
589
- instrumentor=self)
590
- if return_result:
591
- return return_result
600
+ return_result: Any = provider.process_synchronous_response(
601
+ response=response,
602
+ log_prompt_and_response=self._log_prompt_and_response,
603
+ kwargs=kwargs)
604
+
605
+ if return_result:
606
+ return return_result
592
607
 
593
- await self._aingest_units(ingest)
608
+ await self._aingest_units(provider._ingest)
594
609
 
595
610
  return response
596
611
 
597
612
  def chat_wrapper(
598
613
  self,
599
614
  category: str,
600
- process_chunk: Optional[Callable[[Any, IngestUnitsParams], None]],
601
- process_request: Optional[Callable[[IngestUnitsParams, Any, Any], None]],
602
- process_synchronous_response: Any,
615
+ provider: _ProviderRequest,
603
616
  is_streaming: _IsStreaming,
604
617
  wrapped: Any,
605
618
  instance: Any,
@@ -620,7 +633,7 @@ class _PayiInstrumentor:
620
633
 
621
634
  # after _udpate_headers, all metadata to add to ingest is in extra_headers, keyed by the xproxy-xxx header name
622
635
  extra_headers = kwargs.get("extra_headers", {})
623
- self._update_headers(context, extra_headers)
636
+ self._update_extra_headers(context, extra_headers)
624
637
 
625
638
  if context.get("proxy", True):
626
639
  if "extra_headers" not in kwargs:
@@ -628,13 +641,13 @@ class _PayiInstrumentor:
628
641
 
629
642
  return wrapped(*args, **kwargs)
630
643
 
631
- ingest: IngestUnitsParams = {"category": category, "units": {}} # type: ignore
644
+ provider._ingest = {"category": category, "units": {}} # type: ignore
632
645
  if is_bedrock:
633
646
  # boto3 doesn't allow extra_headers
634
647
  kwargs.pop("extra_headers", None)
635
- ingest["resource"] = kwargs.get("modelId", "")
648
+ provider._ingest["resource"] = kwargs.get("modelId", "")
636
649
  else:
637
- ingest["resource"] = kwargs.get("model", "")
650
+ provider._ingest["resource"] = kwargs.get("model", "")
638
651
 
639
652
  if category == PayiCategories.openai and instance and hasattr(instance, "_client"):
640
653
  from .OpenAIInstrumentor import OpenAiInstrumentor # noqa: I001
@@ -652,21 +665,20 @@ class _PayiInstrumentor:
652
665
  logging.error("Azure OpenAI invalid resource scope, not ingesting")
653
666
  return wrapped(*args, **kwargs)
654
667
 
655
- ingest["resource_scope"] = resource_scope
668
+ provider._ingest["resource_scope"] = resource_scope
656
669
 
657
670
  category = PayiCategories.azure_openai
658
671
 
659
- ingest["category"] = category
660
- ingest["resource"] = route_as_resource
672
+ provider._ingest["category"] = category
673
+ provider._ingest["resource"] = route_as_resource
661
674
 
662
675
  current_frame = inspect.currentframe()
663
676
  # f_back excludes the current frame, strip() cleans up whitespace and newlines
664
677
  stack = [frame.strip() for frame in traceback.format_stack(current_frame.f_back)] # type: ignore
665
678
 
666
- ingest['properties'] = { 'system.stack_trace': json.dumps(stack) }
679
+ provider._ingest['properties'] = { 'system.stack_trace': json.dumps(stack) }
667
680
 
668
- if process_request:
669
- process_request(ingest, (), kwargs)
681
+ provider.process_request(kwargs)
670
682
 
671
683
  sw = Stopwatch()
672
684
  stream: bool = False
@@ -679,7 +691,7 @@ class _PayiInstrumentor:
679
691
  stream = False
680
692
 
681
693
  try:
682
- self._prepare_ingest(ingest, extra_headers, **kwargs)
694
+ self._prepare_ingest(provider._ingest, extra_headers, **kwargs)
683
695
  sw.start()
684
696
  response = wrapped(*args, **kwargs)
685
697
 
@@ -697,9 +709,8 @@ class _PayiInstrumentor:
697
709
  instance=instance,
698
710
  instrumentor=self,
699
711
  log_prompt_and_response=self._log_prompt_and_response,
700
- ingest=ingest,
701
712
  stopwatch=sw,
702
- process_chunk=process_chunk,
713
+ provider=provider,
703
714
  is_bedrock=is_bedrock,
704
715
  )
705
716
 
@@ -714,114 +725,97 @@ class _PayiInstrumentor:
714
725
 
715
726
  sw.stop()
716
727
  duration = sw.elapsed_ms_int()
717
- ingest["end_to_end_latency_ms"] = duration
718
- ingest["http_status_code"] = 200
728
+ provider._ingest["end_to_end_latency_ms"] = duration
729
+ provider._ingest["http_status_code"] = 200
719
730
 
720
- if process_synchronous_response:
721
- return_result: Any = process_synchronous_response(
722
- response=response,
723
- ingest=ingest,
724
- log_prompt_and_response=self._log_prompt_and_response,
725
- instrumentor=self)
726
- if return_result:
727
- return return_result
731
+ return_result: Any = provider.process_synchronous_response(
732
+ response=response,
733
+ log_prompt_and_response=self._log_prompt_and_response,
734
+ kwargs=kwargs)
735
+ if return_result:
736
+ return return_result
728
737
 
729
- self._ingest_units(ingest)
738
+ self._ingest_units(provider._ingest)
730
739
 
731
740
  return response
732
741
 
733
742
  @staticmethod
734
- def _update_headers(
743
+ def _update_extra_headers(
735
744
  context: _Context,
736
745
  extra_headers: "dict[str, str]",
737
746
  ) -> None:
738
- limit_ids: Optional[list[str]] = context.get("limit_ids")
739
- request_tags: Optional[list[str]] = context.get("request_tags")
740
- experience_name: Optional[str] = context.get("experience_name")
741
- experience_id: Optional[str] = context.get("experience_id")
742
- use_case_name: Optional[str] = context.get("use_case_name")
743
- use_case_id: Optional[str] = context.get("use_case_id")
744
- use_case_version: Optional[int] = context.get("use_case_version")
745
- user_id: Optional[str] = context.get("user_id")
746
-
747
- # Merge limits from the decorator and extra headers
748
- if limit_ids is not None:
749
- existing_limit_ids = extra_headers.get(PayiHeaderNames.limit_ids, None)
750
-
751
- if not existing_limit_ids:
752
- extra_headers[PayiHeaderNames.limit_ids] = ",".join(limit_ids)
747
+ context_limit_ids: Optional[list[str]] = context.get("limit_ids")
748
+ context_experience_name: Optional[str] = context.get("experience_name")
749
+ context_experience_id: Optional[str] = context.get("experience_id")
750
+ context_use_case_name: Optional[str] = context.get("use_case_name")
751
+ context_use_case_id: Optional[str] = context.get("use_case_id")
752
+ context_use_case_version: Optional[int] = context.get("use_case_version")
753
+ context_user_id: Optional[str] = context.get("user_id")
754
+
755
+ # headers_limit_ids = extra_headers.get(PayiHeaderNames.limit_ids, None)
756
+
757
+ # If the caller specifies limit_ids in extra_headers, it takes precedence over the decorator
758
+ if PayiHeaderNames.limit_ids in extra_headers:
759
+ headers_limit_ids = extra_headers.get(PayiHeaderNames.limit_ids)
760
+
761
+ if headers_limit_ids is None or len(headers_limit_ids) == 0:
762
+ # headers_limit_ids is empty, remove it from extra_headers
763
+ extra_headers.pop(PayiHeaderNames.limit_ids, None)
764
+ else:
765
+ # leave the value in extra_headers
766
+ ...
767
+ elif context_limit_ids:
768
+ extra_headers[PayiHeaderNames.limit_ids] = ",".join(context_limit_ids)
769
+
770
+ if PayiHeaderNames.user_id in extra_headers:
771
+ headers_user_id = extra_headers.get(PayiHeaderNames.user_id, None)
772
+ if headers_user_id is None or len(headers_user_id) == 0:
773
+ # headers_user_id is empty, remove it from extra_headers
774
+ extra_headers.pop(PayiHeaderNames.user_id, None)
753
775
  else:
754
- existing_ids = existing_limit_ids.split(',')
755
- combined_ids = list(set(existing_ids + limit_ids))
756
- extra_headers[PayiHeaderNames.limit_ids] = ",".join(combined_ids)
757
-
758
- # Merge request from the decorator and extra headers
759
- if request_tags is not None:
760
- existing_request_tags = extra_headers.get(PayiHeaderNames.request_tags, None)
761
-
762
- if not existing_request_tags:
763
- extra_headers[PayiHeaderNames.request_tags] = ",".join(request_tags)
776
+ # leave the value in extra_headers
777
+ ...
778
+ elif context_user_id:
779
+ extra_headers[PayiHeaderNames.user_id] = context_user_id
780
+
781
+ if PayiHeaderNames.use_case_name in extra_headers:
782
+ headers_use_case_name = extra_headers.get(PayiHeaderNames.use_case_name, None)
783
+ if headers_use_case_name is None or len(headers_use_case_name) == 0:
784
+ # headers_use_case_name is empty, remove all use case related headers
785
+ extra_headers.pop(PayiHeaderNames.use_case_name, None)
786
+ extra_headers.pop(PayiHeaderNames.use_case_id, None)
787
+ extra_headers.pop(PayiHeaderNames.use_case_version, None)
764
788
  else:
765
- existing_tags = existing_request_tags.split(',')
766
- combined_tags = list(set(existing_tags + request_tags))
767
- extra_headers[PayiHeaderNames.request_tags] = ",".join(combined_tags)
768
-
769
- # inner extra_headers user_id takes precedence over outer decorator user_id
770
- if user_id is not None and extra_headers.get(PayiHeaderNames.user_id, None) is None:
771
- extra_headers[PayiHeaderNames.user_id] = user_id
772
-
773
- # inner extra_headers experience_name and experience_id take precedence over outer decorator experience_name and experience_id
774
- # if either inner value is specified, ignore outer decorator values
775
- if PayiHeaderNames.experience_name not in extra_headers and PayiHeaderNames.experience_id not in extra_headers:
776
-
777
- # use both decorator values
778
- if experience_name is not None:
779
- extra_headers[PayiHeaderNames.experience_name] = experience_name
780
- if experience_id is not None:
781
- extra_headers[PayiHeaderNames.experience_id] = experience_id
782
-
783
- elif PayiHeaderNames.experience_id in extra_headers and PayiHeaderNames.experience_name not in extra_headers:
784
- # use the decorator experience name and the inner experience id
785
- if experience_name is not None:
786
- extra_headers[PayiHeaderNames.experience_name] = experience_name
787
-
788
- else:
789
- # use the inner experience name and id as-is
790
- ...
791
-
792
- # inner extra_headers use_casee_name and use_case_id take precedence over outer decorator use_case_name and use_case_id
793
- # if either inner value is specified, ignore outer decorator values
794
- if PayiHeaderNames.use_case_name not in extra_headers and PayiHeaderNames.use_case_id not in extra_headers:
795
-
796
- # use decorator values
797
- if use_case_name is not None:
798
- extra_headers[PayiHeaderNames.use_case_name] = use_case_name
799
- if use_case_id is not None:
800
- extra_headers[PayiHeaderNames.use_case_id] = use_case_id
801
- if use_case_version is not None:
802
- extra_headers[PayiHeaderNames.use_case_version] = str(use_case_version)
803
-
804
- elif PayiHeaderNames.use_case_id in extra_headers and PayiHeaderNames.use_case_name not in extra_headers:
805
- # use the decorator experience name and the inner experience id
806
- if use_case_name is not None:
807
- extra_headers[PayiHeaderNames.use_case_name] = use_case_name
808
-
809
- # use the decorator experience version and the inner experience id
810
- if use_case_version is not None:
811
- extra_headers[PayiHeaderNames.use_case_version] = str(use_case_version) # TODO use case
812
-
813
- else:
814
- # use the inner experience name and id as-is
815
- ...
789
+ # leave the value in extra_headers
790
+ ...
791
+ elif context_use_case_name:
792
+ extra_headers[PayiHeaderNames.use_case_name] = context_use_case_name
793
+ if context_use_case_id is not None:
794
+ extra_headers[PayiHeaderNames.use_case_id] = context_use_case_id
795
+ if context_use_case_version is not None:
796
+ extra_headers[PayiHeaderNames.use_case_version] = str(context_use_case_version)
797
+
798
+ if PayiHeaderNames.experience_name in extra_headers:
799
+ headers_experience_name = extra_headers.get(PayiHeaderNames.experience_name, None)
800
+ if headers_experience_name is None or len(headers_experience_name) == 0:
801
+ # headers_experience_name is empty, remove all experience related headers
802
+ extra_headers.pop(PayiHeaderNames.experience_name, None)
803
+ extra_headers.pop(PayiHeaderNames.experience_id, None)
804
+ else:
805
+ # leave the value in extra_headers
806
+ ...
807
+ elif context_experience_name is not None:
808
+ extra_headers[PayiHeaderNames.experience_name] = context_experience_name
809
+ if context_experience_id is not None:
810
+ extra_headers[PayiHeaderNames.experience_id] = context_experience_id
816
811
 
817
812
  @staticmethod
818
- def update_for_vision(input: int, units: 'dict[str, Units]') -> int:
819
- if _PayiInstrumentor.estimated_prompt_tokens in units:
820
- prompt_token_estimate: int = units.pop(_PayiInstrumentor.estimated_prompt_tokens)["input"] # type: ignore
821
- vision = input - prompt_token_estimate
813
+ def update_for_vision(input: int, units: 'dict[str, Units]', estimated_prompt_tokens: Optional[int]) -> int:
814
+ if estimated_prompt_tokens:
815
+ vision = input - estimated_prompt_tokens
822
816
  if (vision > 0):
823
817
  units["vision"] = Units(input=vision, output=0)
824
- input = prompt_token_estimate
818
+ input = estimated_prompt_tokens
825
819
 
826
820
  return input
827
821
 
@@ -863,16 +857,15 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
863
857
  response: Any,
864
858
  instance: Any,
865
859
  instrumentor: _PayiInstrumentor,
866
- ingest: IngestUnitsParams,
867
860
  stopwatch: Stopwatch,
868
- process_chunk: Optional[Callable[[Any, IngestUnitsParams], None]] = None,
861
+ provider: _ProviderRequest,
869
862
  log_prompt_and_response: bool = True,
870
863
  is_bedrock: bool = False,
871
864
  ) -> None:
872
865
 
873
866
  bedrock_from_stream: bool = False
874
867
  if is_bedrock:
875
- ingest["provider_response_id"] = response["ResponseMetadata"]["RequestId"]
868
+ provider._ingest["provider_response_id"] = response["ResponseMetadata"]["RequestId"]
876
869
  stream = response.get("stream", None)
877
870
 
878
871
  if stream:
@@ -889,11 +882,10 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
889
882
 
890
883
  self._instrumentor = instrumentor
891
884
  self._stopwatch: Stopwatch = stopwatch
892
- self._ingest: IngestUnitsParams = ingest
893
885
  self._log_prompt_and_response: bool = log_prompt_and_response
894
886
  self._responses: list[str] = []
895
887
 
896
- self._process_chunk: Optional[Callable[[Any, IngestUnitsParams], None]] = process_chunk
888
+ self._provider: _ProviderRequest = provider
897
889
 
898
890
  self._first_token: bool = True
899
891
  self._is_bedrock: bool = is_bedrock
@@ -913,7 +905,7 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
913
905
 
914
906
  def __iter__(self) -> Any:
915
907
  if self._is_bedrock:
916
- # MUST be reside in a separate function so that the yield statement doesn't implicitly return its own iterator and overriding self
908
+ # MUST reside in a separate function so that the yield statement (e.g. the generator) doesn't implicitly return its own iterator and overriding self
917
909
  return self._iter_bedrock()
918
910
  return self
919
911
 
@@ -942,7 +934,9 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
942
934
  self._stop_iteration()
943
935
  raise e
944
936
  else:
945
- self._evaluate_chunk(chunk)
937
+ if self._evaluate_chunk(chunk) == False:
938
+ return self.__next__()
939
+
946
940
  return chunk
947
941
 
948
942
  async def __anext__(self) -> Any:
@@ -953,35 +947,35 @@ class ChatStreamWrapper(ObjectProxy): # type: ignore
953
947
  await self._astop_iteration()
954
948
  raise e
955
949
  else:
956
- self._evaluate_chunk(chunk)
950
+ if self._evaluate_chunk(chunk) == False:
951
+ return await self.__anext__()
957
952
  return chunk
958
953
 
959
- def _evaluate_chunk(self, chunk: Any) -> None:
954
+ def _evaluate_chunk(self, chunk: Any) -> bool:
960
955
  if self._first_token:
961
- self._ingest["time_to_first_token_ms"] = self._stopwatch.elapsed_ms_int()
956
+ self._provider._ingest["time_to_first_token_ms"] = self._stopwatch.elapsed_ms_int()
962
957
  self._first_token = False
963
958
 
964
959
  if self._log_prompt_and_response:
965
960
  self._responses.append(self.chunk_to_json(chunk))
966
961
 
967
- if self._process_chunk:
968
- self._process_chunk(chunk, self._ingest)
962
+ return self._provider.process_chunk(chunk)
969
963
 
970
964
  def _process_stop_iteration(self) -> None:
971
965
  self._stopwatch.stop()
972
- self._ingest["end_to_end_latency_ms"] = self._stopwatch.elapsed_ms_int()
973
- self._ingest["http_status_code"] = 200
966
+ self._provider._ingest["end_to_end_latency_ms"] = self._stopwatch.elapsed_ms_int()
967
+ self._provider._ingest["http_status_code"] = 200
974
968
 
975
969
  if self._log_prompt_and_response:
976
- self._ingest["provider_response_json"] = self._responses
970
+ self._provider._ingest["provider_response_json"] = self._responses
977
971
 
978
972
  async def _astop_iteration(self) -> None:
979
973
  self._process_stop_iteration()
980
- await self._instrumentor._aingest_units(self._ingest)
974
+ await self._instrumentor._aingest_units(self._provider._ingest)
981
975
 
982
976
  def _stop_iteration(self) -> None:
983
977
  self._process_stop_iteration()
984
- self._instrumentor._ingest_units(self._ingest)
978
+ self._instrumentor._ingest_units(self._provider._ingest)
985
979
 
986
980
  @staticmethod
987
981
  def chunk_to_json(chunk: Any) -> str:
@@ -1023,18 +1017,6 @@ def payi_instrument(
1023
1017
  elif isinstance(p, AsyncPayi): # type: ignore
1024
1018
  apayi_param = p
1025
1019
 
1026
- global_context_ingest: Optional[bool] = None
1027
-
1028
- if config is not None:
1029
- if "proxy" not in config:
1030
- config["proxy"] = False
1031
- global_context_ingest = config.get("proxy") == False
1032
-
1033
- # Use default clients if not provided for global ingest instrumentation
1034
- if not payi_param and not apayi_param and global_context_ingest:
1035
- payi_param = Payi()
1036
- apayi_param = AsyncPayi()
1037
-
1038
1020
  # allow for both payi and apayi to be None for the @proxy case
1039
1021
  _instrumentor = _PayiInstrumentor(
1040
1022
  payi=payi_param,
@@ -1047,7 +1029,6 @@ def payi_instrument(
1047
1029
 
1048
1030
  def ingest(
1049
1031
  limit_ids: Optional["list[str]"] = None,
1050
- request_tags: Optional["list[str]"] = None,
1051
1032
  experience_name: Optional[str] = None,
1052
1033
  experience_id: Optional[str] = None,
1053
1034
  use_case_name: Optional[str] = None,
@@ -1066,7 +1047,6 @@ def ingest(
1066
1047
  func,
1067
1048
  False,
1068
1049
  limit_ids,
1069
- request_tags,
1070
1050
  experience_name,
1071
1051
  experience_id,
1072
1052
  use_case_name,
@@ -1085,7 +1065,6 @@ def ingest(
1085
1065
  func,
1086
1066
  False,
1087
1067
  limit_ids,
1088
- request_tags,
1089
1068
  experience_name,
1090
1069
  experience_id,
1091
1070
  use_case_name,
@@ -1100,7 +1079,6 @@ def ingest(
1100
1079
 
1101
1080
  def proxy(
1102
1081
  limit_ids: Optional["list[str]"] = None,
1103
- request_tags: Optional["list[str]"] = None,
1104
1082
  experience_name: Optional[str] = None,
1105
1083
  experience_id: Optional[str] = None,
1106
1084
  use_case_id: Optional[str] = None,
@@ -1118,7 +1096,6 @@ def proxy(
1118
1096
  func,
1119
1097
  True,
1120
1098
  limit_ids,
1121
- request_tags,
1122
1099
  experience_name,
1123
1100
  experience_id,
1124
1101
  use_case_name,
@@ -1138,7 +1115,6 @@ def proxy(
1138
1115
  func,
1139
1116
  True,
1140
1117
  limit_ids,
1141
- request_tags,
1142
1118
  experience_name,
1143
1119
  experience_id,
1144
1120
  use_case_name,